Hello Pop'n Music support
This commit is contained in:
parent
223c93874c
commit
2b6ce21084
@ -21,3 +21,4 @@ include bemani/frontend/static/controllers/ddr/*.js
|
||||
include bemani/frontend/static/controllers/reflec/*.js
|
||||
include bemani/frontend/static/controllers/sdvx/*.js
|
||||
include bemani/frontend/static/controllers/museca/*.js
|
||||
include bemani/frontend/static/controllers/hpnm/*.js
|
||||
|
14
README.md
14
README.md
@ -283,7 +283,7 @@ BEMANI games boot and supports full scores, profile and events for Beatmania IID
|
||||
Pop'n Music 19-26, Jubeat Saucer, Saucer Fulfill, Prop, Qubell, Clan and Festo, Sound
|
||||
Voltex 1, 2, 3 Season 1/2 and 4, Dance Dance Revolution X2, X3, 2013, 2014 and Ace,
|
||||
MÚSECA 1, MÚSECA 1+1/2, MÚSECA Plus, Reflec Beat, Limelight, Colette, groovin'!! Upper,
|
||||
Volzza 1 and Volzza 2, Metal Gear Arcade, and finally The\*BishiBashi. Note that it also
|
||||
Volzza 1 and Volzza 2, Metal Gear Arcade, Hello Pop'n Music and finally The\*BishiBashi. Note that it also
|
||||
has matching support for all Reflec Beat versions as well as MGA. By default, this serves
|
||||
traffic based solely on the database it is configured against. If you federate with
|
||||
other networks using the "Data API" admin page, it will upgrade to serving traffic
|
||||
@ -328,8 +328,8 @@ this will run through and attempt to verify simple operation of that service. No
|
||||
guarantees are made on the accuracy of the emulation though I've strived to be
|
||||
correct. In some cases, I will verify the response, and in other cases I will
|
||||
simply verify that certain things exist so as not to crash a real client. This
|
||||
currently generates traffic emulating Beatmania IIDX 20-26, Pop'n Music 19-26, Jubeat
|
||||
Saucer, Fulfill, Prop, Qubell, Clan and Festo, Sound Voltex 1, 2, 3 Season 1/2 and 4,
|
||||
currently generates traffic emulating Beatmania IIDX 20-26, Pop'n Music 19-26, Hello Pop'n Music
|
||||
Jubeat Saucer, Fulfill, Prop, Qubell, Clan and Festo, Sound Voltex 1, 2, 3 Season 1/2 and 4,
|
||||
Dance Dance Revolution X2, X3, 2013, 2014 and Ace, The\*BishiBashi, MÚSECA 1 and MÚSECA
|
||||
1+1/2, Reflec Beat, Reflec Beat Limelight, Reflec Beat Colette, groovin'!! Upper,
|
||||
Volzza 1 and Volzza 2 ad Metal Gear Arcade and can verify card events and score events
|
||||
@ -508,6 +508,14 @@ If you have more than one XML you want to add, you can run this command with a f
|
||||
./read --config config/server.yaml --series pnm --version omni-24 --bin popn24.dll --folder my/xmls/path
|
||||
```
|
||||
|
||||
### Hello Pop'n Music
|
||||
|
||||
For Hello Pop'n Music, run the tsv file in data folder, giving the correct tsv file:
|
||||
|
||||
```
|
||||
./read --config config/server.yaml --series hpnm --version 1 --tsv data/hellopopn.tsv
|
||||
```
|
||||
|
||||
### Jubeat
|
||||
|
||||
For Jubeat, get the music XML out of the data directory of the mix you are importing,
|
||||
|
7
bemani/backend/hellopopn/__init__.py
Normal file
7
bemani/backend/hellopopn/__init__.py
Normal file
@ -0,0 +1,7 @@
|
||||
from bemani.backend.hellopopn.factory import HelloPopnFactory
|
||||
from bemani.backend.hellopopn.base import HelloPopnBase
|
||||
|
||||
__all__ = [
|
||||
"HelloPopnFactory",
|
||||
"HelloPopnBase",
|
||||
]
|
21
bemani/backend/hellopopn/base.py
Normal file
21
bemani/backend/hellopopn/base.py
Normal file
@ -0,0 +1,21 @@
|
||||
from typing import Optional
|
||||
from typing_extensions import Final
|
||||
from bemani.backend.base import Base
|
||||
from bemani.backend.core import CoreHandler, CardManagerHandler, PASELIHandler
|
||||
from bemani.common import GameConstants, ValidatedDict
|
||||
from bemani.protocol import Node
|
||||
|
||||
|
||||
class HelloPopnBase(CoreHandler, CardManagerHandler, PASELIHandler, Base):
|
||||
"""
|
||||
Base game class for all Hello Pop'n Music versions. Handles common functionality for
|
||||
getting profiles based on refid, creating new profiles, looking up and saving
|
||||
scores.
|
||||
"""
|
||||
|
||||
game = GameConstants.HELLO_POPN
|
||||
|
||||
# Chart type, as saved into/loaded from the DB, and returned to game
|
||||
CHART_TYPE_EASY: Final[int] = 0
|
||||
CHART_TYPE_NORMAL: Final[int] = 1
|
||||
CHART_TYPE_HARD: Final[int] = 2
|
27
bemani/backend/hellopopn/factory.py
Normal file
27
bemani/backend/hellopopn/factory.py
Normal file
@ -0,0 +1,27 @@
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
from bemani.backend.base import Base, Factory
|
||||
from bemani.backend.hellopopn.hellopopn import HelloPopnMusic
|
||||
from bemani.common import Model
|
||||
from bemani.data import Data
|
||||
|
||||
|
||||
class HelloPopnFactory(Factory):
|
||||
|
||||
MANAGED_CLASSES = [
|
||||
HelloPopnMusic,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def register_all(cls) -> None:
|
||||
for game in ['JMP']:
|
||||
Base.register(game, HelloPopnFactory)
|
||||
|
||||
@classmethod
|
||||
def create(cls, data: Data, config: Dict[str, Any], model: Model, parentmodel: Optional[Model]=None) -> Optional[Base]:
|
||||
|
||||
if model.gamecode == 'JMP':
|
||||
return HelloPopnMusic(data, config, model)
|
||||
|
||||
# Unknown game version
|
||||
return None
|
267
bemani/backend/hellopopn/hellopopn.py
Normal file
267
bemani/backend/hellopopn/hellopopn.py
Normal file
@ -0,0 +1,267 @@
|
||||
# vim: set fileencoding=utf-8
|
||||
import copy
|
||||
from typing import Any, Dict
|
||||
|
||||
from bemani.backend.hellopopn.base import HelloPopnBase
|
||||
from bemani.backend.ess import EventLogHandler
|
||||
from bemani.common import ValidatedDict, VersionConstants, Profile
|
||||
from bemani.data import Score
|
||||
from bemani.protocol import Node
|
||||
|
||||
class HelloPopnMusic(
|
||||
EventLogHandler,
|
||||
HelloPopnBase,
|
||||
):
|
||||
name = "Hello! Pop'n Music"
|
||||
version = VersionConstants.HELLO_POPN_MUSIC
|
||||
|
||||
@classmethod
|
||||
|
||||
def get_settings(cls) -> Dict[str, Any]:
|
||||
"""
|
||||
Return all of our front-end modifiably settings.
|
||||
"""
|
||||
return {
|
||||
"ints": [
|
||||
],
|
||||
'bools': [
|
||||
{
|
||||
'name': 'Force Song Unlock',
|
||||
'tip': 'Force unlock all songs.',
|
||||
'category': 'game_config',
|
||||
'setting': 'force_unlock_songs',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def handle_game_common_request(self, request: Node) -> Node:
|
||||
root = Node.void('game')
|
||||
|
||||
flag = Node.void('flag')
|
||||
root.add_child(flag)
|
||||
|
||||
flag.set_attribute("id", '1')
|
||||
flag.set_attribute("s1", '1')
|
||||
flag.set_attribute("s2", '1')
|
||||
flag.set_attribute("t", '1')
|
||||
|
||||
root.add_child(Node.u32("cnt_music", 36))
|
||||
|
||||
return root
|
||||
|
||||
def handle_game_shop_request(self, request: Node) -> Node:
|
||||
root = Node.void('game')
|
||||
|
||||
return root
|
||||
|
||||
def handle_game_new_request(self, request: Node) -> Node:
|
||||
#profile creation
|
||||
root = Node.void('game')
|
||||
|
||||
userid = self.data.remote.user.from_refid(self.game, self.version, request.attribute('refid'))
|
||||
|
||||
defaultprofile = Profile(
|
||||
self.game,
|
||||
self.version,
|
||||
request.attribute('refid'),
|
||||
0,
|
||||
{
|
||||
'name': "なし",
|
||||
'chara': "0",
|
||||
'music_id': "0",
|
||||
'level': "0",
|
||||
'style': "0",
|
||||
'love': "0"
|
||||
},
|
||||
)
|
||||
self.put_profile(userid, defaultprofile)
|
||||
|
||||
return root
|
||||
|
||||
def handle_game_load_request(self, request: Node) -> Node:
|
||||
#Load profile values
|
||||
root = Node.void('game')
|
||||
|
||||
|
||||
userid = self.data.remote.user.from_refid(self.game, self.version, request.attribute('refid'))
|
||||
profile = self.get_profile(userid)
|
||||
|
||||
achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
|
||||
|
||||
game_config = self.get_game_config()
|
||||
force_unlock_songs = game_config.get_bool("force_unlock_songs")
|
||||
#if we send all chara love as max, all songs will be unlocked
|
||||
if force_unlock_songs:
|
||||
for n in range(12):
|
||||
chara = Node.void('chara')
|
||||
chara.set_attribute('id', str(n))
|
||||
chara.set_attribute('love', "5")
|
||||
root.add_child(chara)
|
||||
else:
|
||||
#load chara love progress
|
||||
for achievement in achievements:
|
||||
if achievement.type == 'toki_love':
|
||||
chara = Node.void('chara')
|
||||
chara.set_attribute('id', str(achievement.id))
|
||||
chara.set_attribute('love', achievement.data.get_str('love'))
|
||||
root.add_child(chara)
|
||||
|
||||
|
||||
last = Node.void('last')
|
||||
root.add_child(last)
|
||||
last.set_attribute('chara', profile.get_str('chara'))
|
||||
last.set_attribute('level', profile.get_str('level'))
|
||||
last.set_attribute('music_id', profile.get_str('music_id'))
|
||||
last.set_attribute('style', profile.get_str('style'))
|
||||
|
||||
self.update_play_statistics(userid)
|
||||
|
||||
return root
|
||||
|
||||
def handle_game_load_m_request(self, request: Node) -> Node:
|
||||
#Load scores
|
||||
userid = self.data.remote.user.from_refid(self.game, self.version, request.attribute('refid'))
|
||||
scores = self.data.remote.music.get_scores(self.game, self.version, userid)
|
||||
|
||||
root = Node.void('game')
|
||||
sortedscores: Dict[int, Dict[int, Score]] = {}
|
||||
for score in scores:
|
||||
if score.id not in sortedscores:
|
||||
sortedscores[score.id] = {}
|
||||
sortedscores[score.id][score.chart] = score
|
||||
|
||||
for song in sortedscores:
|
||||
for chart in sortedscores[song]:
|
||||
score = sortedscores[song][chart]
|
||||
|
||||
music = Node.void('music')
|
||||
root.add_child(music)
|
||||
music.set_attribute('music_id', str(score.id))
|
||||
|
||||
style = Node.void('style')
|
||||
music.add_child(style)
|
||||
style.set_attribute('id', "0")
|
||||
|
||||
level = Node.void('level')
|
||||
style.add_child(level)
|
||||
|
||||
level.set_attribute('id', str(score.chart))
|
||||
level.set_attribute('score', str(score.points))
|
||||
level.set_attribute('clear_type', str(score.data.get_int('clear_type')))
|
||||
|
||||
return root
|
||||
|
||||
def handle_game_save_request(self, request: Node) -> Node:
|
||||
#Save profile data
|
||||
root = Node.void('game')
|
||||
|
||||
userid = self.data.remote.user.from_refid(self.game, self.version, request.attribute('refid'))
|
||||
oldprofile = self.get_profile(userid)
|
||||
|
||||
newprofile = copy.deepcopy(oldprofile)
|
||||
|
||||
last = request.child('last')
|
||||
newprofile.replace_str('chara', last.attribute('chara'))
|
||||
newprofile.replace_str('level', last.attribute('level'))
|
||||
newprofile.replace_str('music_id', last.attribute('music_id'))
|
||||
newprofile.replace_str('style', last.attribute('style'))
|
||||
newprofile.replace_str('love', last.attribute('love'))
|
||||
|
||||
self.put_profile(userid, newprofile)
|
||||
|
||||
|
||||
game_config = self.get_game_config()
|
||||
force_unlock_songs = game_config.get_bool("force_unlock_songs")
|
||||
#if we were on force unlock mode, achievements will not be modified
|
||||
if force_unlock_songs is False:
|
||||
achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
|
||||
love = last.attribute('love')
|
||||
for achievement in achievements:
|
||||
if achievement.type == 'toki_love' and achievement.id == int(last.attribute('chara')):
|
||||
love = str(int(achievement.data["love"]) + 1)
|
||||
if achievement.data["love"] == "5":
|
||||
love = "5"
|
||||
|
||||
|
||||
self.data.local.user.put_achievement(
|
||||
self.game,
|
||||
self.version,
|
||||
userid,
|
||||
int(last.attribute('chara')),
|
||||
'toki_love',
|
||||
{
|
||||
'love': love,
|
||||
},
|
||||
)
|
||||
|
||||
return root
|
||||
|
||||
def handle_game_save_m_request(self, request: Node) -> Node:
|
||||
#Score saving
|
||||
|
||||
clear_type = int(request.attribute('clear_type'))
|
||||
level = int(request.attribute('level'))
|
||||
songid = int(request.attribute('music_id'))
|
||||
refid = request.attribute('refid')
|
||||
points = int(request.attribute('score'))
|
||||
|
||||
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
||||
|
||||
#pull old score
|
||||
oldscore = self.data.local.music.get_score(
|
||||
self.game,
|
||||
self.version,
|
||||
userid,
|
||||
songid,
|
||||
level,
|
||||
)
|
||||
|
||||
history = ValidatedDict({})
|
||||
|
||||
if oldscore is None:
|
||||
# If it is a new score, create a new dictionary to add to
|
||||
scoredata = ValidatedDict({})
|
||||
highscore = True
|
||||
else:
|
||||
# Set the score to any new record achieved
|
||||
highscore = points >= oldscore.points
|
||||
points = max(oldscore.points, points)
|
||||
scoredata = oldscore.data
|
||||
|
||||
#Clear type
|
||||
scoredata.replace_int('clear_type', max(scoredata.get_int('clear_type'), clear_type))
|
||||
history.replace_int('clear_type', clear_type)
|
||||
|
||||
# Look up where this score was earned
|
||||
lid = self.get_machine_id()
|
||||
|
||||
|
||||
# Write the new score back
|
||||
self.data.local.music.put_score(
|
||||
self.game,
|
||||
self.version,
|
||||
userid,
|
||||
songid,
|
||||
level,
|
||||
lid,
|
||||
points,
|
||||
scoredata,
|
||||
highscore,
|
||||
)
|
||||
|
||||
# Save score history
|
||||
self.data.local.music.put_attempt(
|
||||
self.game,
|
||||
self.version,
|
||||
userid,
|
||||
songid,
|
||||
level,
|
||||
lid,
|
||||
points,
|
||||
history,
|
||||
highscore,
|
||||
)
|
||||
|
||||
root = Node.void('game')
|
||||
|
||||
return root
|
6
bemani/client/hellopopn/__init__.py
Normal file
6
bemani/client/hellopopn/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from bemani.client.hellopopn.hellopopn import HelloPopnMuiscClient
|
||||
|
||||
|
||||
__all__ = [
|
||||
"HelloPopnMuiscClient",
|
||||
]
|
211
bemani/client/hellopopn/hellopopn.py
Normal file
211
bemani/client/hellopopn/hellopopn.py
Normal file
@ -0,0 +1,211 @@
|
||||
import base64
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from bemani.client.base import BaseClient
|
||||
from bemani.protocol import Node
|
||||
|
||||
|
||||
class HelloPopnMuiscClient(BaseClient):
|
||||
|
||||
NAME = "TEST"
|
||||
|
||||
def verify_game_shop(self)->None:
|
||||
call = self.call_node()
|
||||
game = Node.void("game")
|
||||
call.add_child(game)
|
||||
game.set_attribute("accuracy", "0")
|
||||
game.set_attribute("area", "13")
|
||||
game.set_attribute("boot", "51")
|
||||
game.set_attribute("coin", "01.01.--.--.01")
|
||||
game.set_attribute("country", "JP")
|
||||
game.set_attribute("diff", "3")
|
||||
game.set_attribute("first", "1")
|
||||
game.set_attribute("ip", "127.0.0.1")
|
||||
game.set_attribute("is_paseli", "0")
|
||||
game.set_attribute("latitude", "0")
|
||||
game.set_attribute("lineid", ".")
|
||||
game.set_attribute("locid", "JP-7")
|
||||
game.set_attribute("loctype", "0")
|
||||
game.set_attribute("longitude", "0")
|
||||
game.set_attribute("mac", "1:2:3:4:5")
|
||||
game.set_attribute("method", "shop")
|
||||
game.set_attribute("name", ".")
|
||||
game.set_attribute("open_flag", "1")
|
||||
game.set_attribute("pay", "0")
|
||||
game.set_attribute("region", "JP-13")
|
||||
game.set_attribute("soft", "JMP:J:A:A:2014122500")
|
||||
game.set_attribute("softid", "1000")
|
||||
game.set_attribute("stage", "1")
|
||||
game.set_attribute("time", "90")
|
||||
game.set_attribute("ver", "0")
|
||||
|
||||
# Swap with server
|
||||
resp = self.exchange("", call)
|
||||
|
||||
# Verify that response is correct
|
||||
self.assert_path(resp, "response/game/@status")
|
||||
|
||||
|
||||
def verify_game_common(self)->None:
|
||||
call = self.call_node()
|
||||
game = Node.void("game")
|
||||
call.add_child(game)
|
||||
game.set_attribute("method", "common")
|
||||
game.set_attribute("ver", "0")
|
||||
# Swap with server
|
||||
resp = self.exchange("", call)
|
||||
|
||||
# Verify that response is correct
|
||||
#self.__verify_profile(resp)
|
||||
|
||||
def verify_game_new(self, loc: str, refid: str)->None:
|
||||
call = self.call_node()
|
||||
game = Node.void("game")
|
||||
call.add_child(game)
|
||||
game.set_attribute("locid", loc)
|
||||
game.set_attribute("method", "new")
|
||||
game.set_attribute("refid", refid)
|
||||
game.set_attribute("ver", "0")
|
||||
|
||||
# Swap with server
|
||||
resp = self.exchange("", call)
|
||||
|
||||
# Verify that response is correct
|
||||
self.assert_path(resp, "response/game/@status")
|
||||
|
||||
def verify_game_load(self, refid: str)->None:
|
||||
call = self.call_node()
|
||||
game = Node.void("game")
|
||||
call.add_child(game)
|
||||
game.set_attribute("method", "load")
|
||||
game.set_attribute("refid", refid)
|
||||
game.set_attribute("ver", "0")
|
||||
|
||||
# Swap with server
|
||||
resp = self.exchange("", call)
|
||||
|
||||
# Verify that response is correct
|
||||
self.assert_path(resp, "response/game/last/@chara")
|
||||
self.assert_path(resp, "response/game/last/@level")
|
||||
self.assert_path(resp, "response/game/last/@music_id")
|
||||
self.assert_path(resp, "response/game/last/@style")
|
||||
|
||||
def verify_game_load_m(self, refid: str)->None:
|
||||
call = self.call_node()
|
||||
game = Node.void("game")
|
||||
call.add_child(game)
|
||||
game.set_attribute("method", "load_m")
|
||||
game.set_attribute("refid", refid)
|
||||
game.set_attribute("ver", "0")
|
||||
|
||||
# Swap with server
|
||||
resp = self.exchange("", call)
|
||||
|
||||
# Verify that response is correct
|
||||
self.assert_path(resp, "response/game/@status")
|
||||
|
||||
def verify_game_save_m(self, refid: str)->None:
|
||||
call = self.call_node()
|
||||
game = Node.void("game")
|
||||
call.add_child(game)
|
||||
game.set_attribute("clear_type", "2")
|
||||
game.set_attribute("level", "1")
|
||||
game.set_attribute("method", "save_m")
|
||||
game.set_attribute("music_id", "1")
|
||||
game.set_attribute("refid", refid)
|
||||
game.set_attribute("score", "736000")
|
||||
game.set_attribute("style", "0")
|
||||
game.set_attribute("ver", "0")
|
||||
|
||||
# Swap with server
|
||||
resp = self.exchange("", call)
|
||||
|
||||
# Verify that response is correct
|
||||
self.assert_path(resp, "response/game/@status")
|
||||
|
||||
def verify_game_save(self, loc: str , refid: str)->None:
|
||||
call = self.call_node()
|
||||
game = Node.void("game")
|
||||
call.add_child(game)
|
||||
game.set_attribute("locid", loc)
|
||||
game.set_attribute("method", "save")
|
||||
game.set_attribute("refid", refid)
|
||||
game.set_attribute("ver", "0")
|
||||
last = Node.void("last")
|
||||
game.add_child(last)
|
||||
last.set_attribute("chara", "1")
|
||||
last.set_attribute("level", "1")
|
||||
last.set_attribute("love", "1")
|
||||
last.set_attribute("music_id", "1")
|
||||
last.set_attribute("style", "0")
|
||||
|
||||
# Swap with server
|
||||
resp = self.exchange("", call)
|
||||
|
||||
# Verify that response is correct
|
||||
self.assert_path(resp, "response/game/@status")
|
||||
|
||||
|
||||
def verify(self, cardid: Optional[str]) -> None:
|
||||
# Verify boot sequence is okay
|
||||
self.verify_services_get(
|
||||
expected_services=[
|
||||
"cardmng",
|
||||
"dlstatus",
|
||||
"eacoin",
|
||||
"facility",
|
||||
"lobby",
|
||||
"local",
|
||||
"message",
|
||||
"package",
|
||||
"pcbevent",
|
||||
"pcbtracker",
|
||||
"pkglist",
|
||||
"posevent",
|
||||
"ntp",
|
||||
"keepalive",
|
||||
]
|
||||
)
|
||||
paseli_enabled = self.verify_pcbtracker_alive()
|
||||
self.verify_package_list()
|
||||
self.verify_message_get()
|
||||
location = self.verify_facility_get()
|
||||
self.verify_pcbevent_put()
|
||||
self.verify_game_shop()
|
||||
self.verify_game_common()
|
||||
|
||||
# Verify card registration and profile lookup
|
||||
if cardid is not None:
|
||||
card = cardid
|
||||
else:
|
||||
card = self.random_card()
|
||||
print(f"Generated random card ID {card} for use.")
|
||||
|
||||
if cardid is None:
|
||||
self.verify_cardmng_inquire(card, msg_type='unregistered', paseli_enabled=paseli_enabled)
|
||||
self.verify_pcbevent_put()
|
||||
ref_id = self.verify_cardmng_getrefid(card)
|
||||
if len(ref_id) != 16:
|
||||
raise Exception(f'Invalid refid \'{ref_id}\' returned when registering card')
|
||||
if ref_id != self.verify_cardmng_inquire(card, msg_type='new', paseli_enabled=paseli_enabled):
|
||||
raise Exception(f'Invalid refid \'{ref_id}\' returned when querying card')
|
||||
#New profile creation
|
||||
self.verify_game_new(location,ref_id)
|
||||
else:
|
||||
print("Skipping new card checks for existing card")
|
||||
ref_id = self.verify_cardmng_inquire(card, msg_type='query', paseli_enabled=paseli_enabled)
|
||||
|
||||
# Verify pin handling and return card handling
|
||||
self.verify_cardmng_authpass(ref_id, correct=True)
|
||||
self.verify_cardmng_authpass(ref_id, correct=False)
|
||||
|
||||
if ref_id != self.verify_cardmng_inquire(card, msg_type='query', paseli_enabled=paseli_enabled):
|
||||
raise Exception(f'Invalid refid \'{ref_id}\' returned when querying card')
|
||||
self.verify_game_load(ref_id)
|
||||
self.verify_game_load_m(ref_id)
|
||||
self.verify_game_save_m(ref_id)
|
||||
self.verify_game_save(location,ref_id)
|
||||
|
||||
|
||||
|
@ -21,6 +21,7 @@ class GameConstants(Enum):
|
||||
POPN_MUSIC: Final[str] = "pnm"
|
||||
REFLEC_BEAT: Final[str] = "reflec"
|
||||
SDVX: Final[str] = "sdvx"
|
||||
HELLO_POPN: Final[str] = 'hpnm'
|
||||
|
||||
|
||||
class VersionConstants:
|
||||
@ -139,6 +140,8 @@ class VersionConstants:
|
||||
SDVX_GRAVITY_WARS: Final[int] = 3
|
||||
SDVX_HEAVENLY_HAVEN: Final[int] = 4
|
||||
|
||||
HELLO_POPN_MUSIC: Final[int] = 1
|
||||
|
||||
|
||||
class APIConstants(Enum):
|
||||
"""
|
||||
|
@ -279,6 +279,7 @@ def viewmachines() -> Response:
|
||||
GameConstants.POPN_MUSIC.value: "Pop'n Music",
|
||||
GameConstants.REFLEC_BEAT.value: "Reflec Beat",
|
||||
GameConstants.SDVX.value: "SDVX",
|
||||
GameConstants.HELLO_POPN.value: "Hello Pop'n Music",
|
||||
},
|
||||
"games": games,
|
||||
"enforcing": g.config.server.enforce_pcbid,
|
||||
|
@ -813,6 +813,51 @@ def navigation() -> Dict[str, Any]:
|
||||
},
|
||||
)
|
||||
|
||||
if GameConstants.HELLO_POPN in g.config.support:
|
||||
# Hello Pop'n Music pages
|
||||
hello_popn_entries = []
|
||||
if len([p for p in profiles if p[0] == GameConstants.HELLO_POPN]) > 0:
|
||||
hello_popn_entries.extend([
|
||||
{
|
||||
"label": "Game Options",
|
||||
"uri": url_for("hpnm_pages.viewsettings"),
|
||||
},
|
||||
{
|
||||
"label": "Personal Profile",
|
||||
"uri": url_for("hpnm_pages.viewplayer", userid=g.userID),
|
||||
},
|
||||
{
|
||||
'label': 'Personal Scores',
|
||||
'uri': url_for('hpnm_pages.viewscores', userid=g.userID),
|
||||
},
|
||||
{
|
||||
"label": "Personal Records",
|
||||
"uri": url_for("hpnm_pages.viewrecords", userid=g.userID),
|
||||
},
|
||||
])
|
||||
hello_popn_entries.extend([
|
||||
{
|
||||
'label': 'Global Scores',
|
||||
'uri': url_for('hpnm_pages.viewnetworkscores'),
|
||||
},
|
||||
{
|
||||
"label": "Global Records",
|
||||
"uri": url_for("hpnm_pages.viewnetworkrecords"),
|
||||
},
|
||||
{
|
||||
'label': 'All Players',
|
||||
'uri': url_for('hpnm_pages.viewplayers'),
|
||||
},
|
||||
])
|
||||
pages.append(
|
||||
{
|
||||
'label': "Hello Pop'n Music",
|
||||
'entries': hello_popn_entries,
|
||||
'base_uri': app.blueprints['hpnm_pages'].url_prefix,
|
||||
'gamecode': GameConstants.HELLO_POPN,
|
||||
},
|
||||
)
|
||||
|
||||
# Admin pages
|
||||
if user.admin:
|
||||
pages.append(
|
||||
|
@ -45,6 +45,7 @@ def format_machine(machine: Machine) -> Dict[str, Any]:
|
||||
GameConstants.POPN_MUSIC: "Pop'n Music",
|
||||
GameConstants.REFLEC_BEAT: "Reflec Beat",
|
||||
GameConstants.SDVX: "SDVX",
|
||||
GameConstants.HELLO_POPN: "Hello Pop'n Music",
|
||||
}.get(machine.game)
|
||||
elif machine.version > 0:
|
||||
game = [
|
||||
|
8
bemani/frontend/hellopopn/__init__.py
Normal file
8
bemani/frontend/hellopopn/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
from bemani.frontend.hellopopn.endpoints import hpnm_pages
|
||||
from bemani.frontend.hellopopn.cache import HelloPopnMusicCache
|
||||
|
||||
|
||||
__all__ = [
|
||||
"HelloPopnMusicCache",
|
||||
"hpnm_pages",
|
||||
]
|
19
bemani/frontend/hellopopn/cache.py
Normal file
19
bemani/frontend/hellopopn/cache.py
Normal file
@ -0,0 +1,19 @@
|
||||
from bemani.data import Config, Data
|
||||
from flask_caching import Cache
|
||||
from bemani.frontend.app import app
|
||||
|
||||
from bemani.frontend.hellopopn.hellopopn import HelloPopnMusicFrontend
|
||||
|
||||
class HelloPopnMusicCache:
|
||||
|
||||
@classmethod
|
||||
def preload(cls, data: Data, config: Config) -> None:
|
||||
cache = Cache(
|
||||
app,
|
||||
config={
|
||||
"CACHE_TYPE": "filesystem",
|
||||
"CACHE_DIR": config.cache_dir,
|
||||
},
|
||||
)
|
||||
frontend = HelloPopnMusicFrontend(data, config, cache)
|
||||
frontend.get_all_songs(force_db_load=True)
|
345
bemani/frontend/hellopopn/endpoints.py
Normal file
345
bemani/frontend/hellopopn/endpoints.py
Normal file
@ -0,0 +1,345 @@
|
||||
import re
|
||||
from flask import Blueprint
|
||||
from bemani.frontend.templates import templates_location
|
||||
from bemani.frontend.static import static_location
|
||||
from bemani.data import UserID
|
||||
from typing import Any, Dict
|
||||
from flask import Blueprint, request, Response, url_for, abort
|
||||
from bemani.frontend.app import loginrequired, jsonify, render_react
|
||||
from bemani.frontend.hellopopn.hellopopn import HelloPopnMusicFrontend
|
||||
from bemani.frontend.types import g
|
||||
from bemani.common import GameConstants
|
||||
|
||||
hpnm_pages = Blueprint(
|
||||
'hpnm_pages',
|
||||
__name__,
|
||||
url_prefix='/hpnm',
|
||||
template_folder=templates_location,
|
||||
static_folder=static_location,
|
||||
)
|
||||
|
||||
|
||||
@hpnm_pages.route("/scores")
|
||||
@loginrequired
|
||||
def viewnetworkscores() -> Response:
|
||||
# Only load the last 100 results for the initial fetch, so we can render faster
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
network_scores = frontend.get_network_scores(limit=100)
|
||||
|
||||
if len(network_scores["attempts"]) > 10:
|
||||
network_scores["attempts"] = frontend.round_to_ten(network_scores["attempts"])
|
||||
|
||||
|
||||
return render_react(
|
||||
"Global Hello Pop'n Music Scores",
|
||||
"hpnm/scores.react.js",
|
||||
{
|
||||
"attempts": network_scores["attempts"],
|
||||
"songs": frontend.get_all_songs(),
|
||||
"players": network_scores["players"],
|
||||
"shownames": True,
|
||||
"shownewrecords": False,
|
||||
},
|
||||
{
|
||||
"refresh": url_for("hpnm_pages.listnetworkscores"),
|
||||
"player": url_for("hpnm_pages.viewplayer", userid=-1),
|
||||
"individual_score": url_for("hpnm_pages.viewtopscores", musicid=-1),
|
||||
},
|
||||
)
|
||||
|
||||
@hpnm_pages.route("/scores/list")
|
||||
@jsonify
|
||||
@loginrequired
|
||||
def listnetworkscores() -> Dict[str, Any]:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
return frontend.get_network_scores()
|
||||
|
||||
@hpnm_pages.route('/scores/<int:userid>')
|
||||
@loginrequired
|
||||
def viewscores(userid: UserID) -> Response:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
info = frontend.get_latest_player_info([userid]).get(userid)
|
||||
if info is None:
|
||||
abort(404)
|
||||
scores = frontend.get_scores(userid,limit=100)
|
||||
|
||||
if len(scores) > 10:
|
||||
scores = frontend.round_to_ten(scores)
|
||||
|
||||
return render_react(
|
||||
"Global Hello Pop'n Music Scores",
|
||||
"hpnm/scores.react.js",
|
||||
{
|
||||
"attempts": scores,
|
||||
"songs": frontend.get_all_songs(),
|
||||
"players": {},
|
||||
"shownames": False,
|
||||
"shownewrecords": True,
|
||||
},
|
||||
{
|
||||
"refresh": url_for("hpnm_pages.listscores", userid=userid),
|
||||
"player": url_for("hpnm_pages.viewplayer", userid=-1),
|
||||
"individual_score": url_for("hpnm_pages.viewtopscores", musicid=-1),
|
||||
},
|
||||
)
|
||||
|
||||
@hpnm_pages.route("/scores/<int:userid>/list")
|
||||
@jsonify
|
||||
@loginrequired
|
||||
def listscores(userid: UserID) -> Dict[str, Any]:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
return {
|
||||
"attempts": frontend.get_scores(userid),
|
||||
"players": {},
|
||||
}
|
||||
|
||||
@hpnm_pages.route("/topscores/<int:musicid>")
|
||||
@loginrequired
|
||||
def viewtopscores(musicid: int) -> Response:
|
||||
# We just want to find the latest mix that this song exists in
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
versions = sorted(
|
||||
[version for (game, version, name) in frontend.all_games()],
|
||||
reverse=True,
|
||||
)
|
||||
name = None
|
||||
artist = None
|
||||
|
||||
for version in versions:
|
||||
for chart in [0, 1, 2]:
|
||||
details = g.data.local.music.get_song(GameConstants.HELLO_POPN, version, musicid, chart)
|
||||
if details is not None:
|
||||
if name is None:
|
||||
name = details.name
|
||||
if artist is None:
|
||||
artist = details.artist
|
||||
|
||||
if name is None:
|
||||
# Not a real song!
|
||||
abort(404)
|
||||
|
||||
top_scores = frontend.get_top_scores(musicid)
|
||||
|
||||
return render_react(
|
||||
f"Top Hello Pop'n Music Scores for {artist} - {name}",
|
||||
"hpnm/topscores.react.js",
|
||||
{
|
||||
"name": name,
|
||||
"artist": artist,
|
||||
"players": top_scores["players"],
|
||||
"topscores": top_scores["topscores"],
|
||||
},
|
||||
{
|
||||
"refresh": url_for("hpnm_pages.listtopscores", musicid=musicid),
|
||||
"player": url_for("hpnm_pages.viewplayer", userid=-1),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@hpnm_pages.route("/topscores/<int:musicid>/list")
|
||||
@jsonify
|
||||
@loginrequired
|
||||
def listtopscores(musicid: int) -> Dict[str, Any]:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
return frontend.get_top_scores(musicid)
|
||||
|
||||
|
||||
@hpnm_pages.route('/players')
|
||||
@loginrequired
|
||||
def viewplayers() -> Response:
|
||||
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
return render_react(
|
||||
"All Hello Pop'n Music Players",
|
||||
'hpnm/allplayers.react.js',
|
||||
{
|
||||
'players': frontend.get_all_players()
|
||||
},
|
||||
{
|
||||
'refresh': url_for('hpnm_pages.listplayers'),
|
||||
'player': url_for('hpnm_pages.viewplayer', userid=-1),
|
||||
},
|
||||
)
|
||||
|
||||
@hpnm_pages.route('/players/list')
|
||||
@jsonify
|
||||
@loginrequired
|
||||
def listplayers() -> Dict[str, Any]:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
return {
|
||||
'players': frontend.get_all_players(),
|
||||
}
|
||||
|
||||
|
||||
@hpnm_pages.route("/players/<int:userid>")
|
||||
@loginrequired
|
||||
def viewplayer(userid: UserID) -> Response:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
info = frontend.get_all_player_info([userid])[userid]
|
||||
if not info:
|
||||
abort(404)
|
||||
latest_version = sorted(info.keys(), reverse=True)[0]
|
||||
|
||||
return render_react(
|
||||
f'{info[latest_version]["name"]}\'s Hello Pop\'n Music Profile',
|
||||
"hpnm/player.react.js",
|
||||
{
|
||||
"playerid": userid,
|
||||
"own_profile": userid == g.userID,
|
||||
"player": info,
|
||||
"versions": {version: name for (game, version, name) in frontend.all_games()},
|
||||
},
|
||||
{
|
||||
"refresh": url_for("hpnm_pages.listplayer", userid=userid),
|
||||
"records": url_for("hpnm_pages.viewrecords", userid=userid),
|
||||
"scores": url_for("hpnm_pages.viewscores", userid=userid),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@hpnm_pages.route("/players/<int:userid>/list")
|
||||
@jsonify
|
||||
@loginrequired
|
||||
def listplayer(userid: UserID) -> Dict[str, Any]:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
info = frontend.get_all_player_info([userid])[userid]
|
||||
|
||||
return {
|
||||
"player": info,
|
||||
}
|
||||
|
||||
@hpnm_pages.route("/options")
|
||||
@loginrequired
|
||||
def viewsettings() -> Response:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
userid = g.userID
|
||||
info = frontend.get_all_player_info([userid])[userid]
|
||||
if not info:
|
||||
abort(404)
|
||||
|
||||
return render_react(
|
||||
"Hello Pop'n Music Game Settings",
|
||||
"hpnm/settings.react.js",
|
||||
{
|
||||
"player": info,
|
||||
"versions": {version: name for (game, version, name) in frontend.all_games()},
|
||||
},
|
||||
{
|
||||
"updatename": url_for("hpnm_pages.updatename"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@hpnm_pages.route("/options/name/update", methods=["POST"])
|
||||
@jsonify
|
||||
@loginrequired
|
||||
def updatename() -> Dict[str, Any]:
|
||||
version = int(request.get_json()["version"])
|
||||
name = request.get_json()["name"]
|
||||
user = g.data.local.user.get_user(g.userID)
|
||||
if user is None:
|
||||
raise Exception("Unable to find user to update!")
|
||||
|
||||
# Grab profile and update name
|
||||
profile = g.data.local.user.get_profile(GameConstants.HELLO_POPN, version, user.id)
|
||||
if profile is None:
|
||||
raise Exception("Unable to find profile to update!")
|
||||
if len(name) == 0 or len(name) > 6:
|
||||
raise Exception("Invalid profile name!")
|
||||
if (
|
||||
re.match(
|
||||
"^["
|
||||
+ "\uFF20-\uFF3A"
|
||||
+ "\uFF41-\uFF5A" # widetext A-Z and @
|
||||
+ "\uFF10-\uFF19" # widetext a-z
|
||||
+ "\uFF0C\uFF0E\uFF3F" # widetext 0-9
|
||||
+ "\u3041-\u308D\u308F\u3092\u3093" # widetext ,._
|
||||
+ "\u30A1-\u30ED\u30EF\u30F2\u30F3\u30FC" # hiragana
|
||||
+ "]*$", # katakana
|
||||
name,
|
||||
)
|
||||
is None
|
||||
):
|
||||
raise Exception("Invalid profile name!")
|
||||
profile.replace_str("name", name)
|
||||
g.data.local.user.put_profile(GameConstants.HELLO_POPN, version, user.id, profile)
|
||||
|
||||
# Return that we updated
|
||||
return {
|
||||
"version": version,
|
||||
"name": name,
|
||||
}
|
||||
|
||||
@hpnm_pages.route("/records")
|
||||
@loginrequired
|
||||
def viewnetworkrecords() -> Response:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
network_records = frontend.get_network_records()
|
||||
versions = {version: name for (game, version, name) in frontend.all_games()}
|
||||
|
||||
return render_react(
|
||||
"Global Hello Pop'n Music Records",
|
||||
"hpnm/records.react.js",
|
||||
{
|
||||
"records": network_records["records"],
|
||||
"songs": frontend.get_all_songs(),
|
||||
"players": network_records["players"],
|
||||
"versions": versions,
|
||||
"shownames": True,
|
||||
"showpersonalsort": False,
|
||||
"filterempty": False,
|
||||
},
|
||||
{
|
||||
"refresh": url_for("hpnm_pages.listnetworkrecords"),
|
||||
"player": url_for("hpnm_pages.viewplayer", userid=-1),
|
||||
"individual_score": url_for("hpnm_pages.viewtopscores", musicid=-1),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@hpnm_pages.route("/records/list")
|
||||
@jsonify
|
||||
@loginrequired
|
||||
def listnetworkrecords() -> Dict[str, Any]:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
return frontend.get_network_records()
|
||||
|
||||
|
||||
@hpnm_pages.route("/records/<int:userid>")
|
||||
@loginrequired
|
||||
def viewrecords(userid: UserID) -> Response:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
info = frontend.get_latest_player_info([userid]).get(userid)
|
||||
if info is None:
|
||||
abort(404)
|
||||
versions = {version: name for (game, version, name) in frontend.all_games()}
|
||||
|
||||
return render_react(
|
||||
f'{info["name"]}\'s Hello Pop\'n Music Records',
|
||||
"hpnm/records.react.js",
|
||||
{
|
||||
"records": frontend.get_records(userid),
|
||||
"songs": frontend.get_all_songs(),
|
||||
"players": {},
|
||||
"versions": versions,
|
||||
"shownames": False,
|
||||
"showpersonalsort": True,
|
||||
"filterempty": True,
|
||||
},
|
||||
{
|
||||
"refresh": url_for("hpnm_pages.listrecords", userid=userid),
|
||||
"player": url_for("hpnm_pages.viewplayer", userid=-1),
|
||||
"individual_score": url_for("hpnm_pages.viewtopscores", musicid=-1),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@hpnm_pages.route("/records/<int:userid>/list")
|
||||
@jsonify
|
||||
@loginrequired
|
||||
def listrecords(userid: UserID) -> Dict[str, Any]:
|
||||
frontend = HelloPopnMusicFrontend(g.data, g.config, g.cache)
|
||||
return {
|
||||
"records": frontend.get_records(userid),
|
||||
"players": {},
|
||||
}
|
58
bemani/frontend/hellopopn/hellopopn.py
Normal file
58
bemani/frontend/hellopopn/hellopopn.py
Normal file
@ -0,0 +1,58 @@
|
||||
import copy
|
||||
from typing import Any, Dict, Iterator, List, Tuple
|
||||
from flask_caching import Cache # type: ignore
|
||||
from bemani.backend.hellopopn import HelloPopnFactory,HelloPopnBase
|
||||
from bemani.common import Profile, ValidatedDict, GameConstants
|
||||
from bemani.data import Data,UserID,Attempt, Song
|
||||
from bemani.frontend.base import FrontendBase
|
||||
|
||||
class HelloPopnMusicFrontend(FrontendBase):
|
||||
|
||||
game = GameConstants.HELLO_POPN
|
||||
|
||||
valid_charts: List[int] = [
|
||||
HelloPopnBase.CHART_TYPE_EASY,
|
||||
HelloPopnBase.CHART_TYPE_NORMAL,
|
||||
HelloPopnBase.CHART_TYPE_HARD,
|
||||
]
|
||||
|
||||
def __init__(self, data: Data, config: Dict[str, Any], cache: Cache) -> None:
|
||||
super().__init__(data, config, cache)
|
||||
self.machines: Dict[int, str] = {}
|
||||
|
||||
def all_games(self) -> Iterator[Tuple[GameConstants, int, str]]:
|
||||
yield from HelloPopnFactory.all_games()
|
||||
|
||||
def format_profile(
|
||||
self, profile: Profile, playstats: ValidatedDict
|
||||
) -> Dict[str, Any]:
|
||||
formatted_profile = super().format_profile(profile, playstats)
|
||||
formatted_profile["plays"] = playstats.get_int("total_plays")
|
||||
formatted_profile["love"] = profile.get_str("love")
|
||||
formatted_profile["level"] = profile.get_str("level")
|
||||
return formatted_profile
|
||||
|
||||
def format_attempt(self, userid: UserID, attempt: Attempt) -> Dict[str, Any]:
|
||||
formatted_attempt = super().format_attempt(userid, attempt)
|
||||
formatted_attempt["clear_type"] = attempt.data.get_int("clear_type")
|
||||
return formatted_attempt
|
||||
|
||||
def format_score(self, userid: UserID, attempt: Attempt) -> Dict[str, Any]:
|
||||
formatted_score = super().format_score(userid, attempt)
|
||||
formatted_score["clear_type"] = attempt.data.get_int("clear_type")
|
||||
return formatted_score
|
||||
|
||||
def format_song(self, song: Song) -> Dict[str, Any]:
|
||||
difficulties = [0, 0, 0]
|
||||
difficulties[song.chart] = song.data.get_int("difficulty", 1)
|
||||
|
||||
formatted_song = super().format_song(song)
|
||||
formatted_song["category"] = song.data.get_int("category", 1)
|
||||
formatted_song["difficulties"] = difficulties
|
||||
return formatted_song
|
||||
|
||||
def merge_song(self, existing: Dict[str, Any], new: Song) -> Dict[str, Any]:
|
||||
new_song = super().merge_song(existing, new)
|
||||
if existing["difficulties"][new.chart] == 0:
|
||||
new_song["difficulties"][new.chart] = new.data.get_int("difficulty", 1)
|
||||
return new_song
|
96
bemani/frontend/static/controllers/hpnm/allplayers.react.js
Normal file
96
bemani/frontend/static/controllers/hpnm/allplayers.react.js
Normal file
@ -0,0 +1,96 @@
|
||||
/*** @jsx React.DOM */
|
||||
|
||||
var all_players = createReactClass({
|
||||
getInitialState: function(props) {
|
||||
return {
|
||||
players: window.players,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.refreshPlayers();
|
||||
},
|
||||
|
||||
refreshPlayers: function() {
|
||||
AJAX.get(
|
||||
Link.get('refresh'),
|
||||
function(response) {
|
||||
this.setState({
|
||||
players: response.players,
|
||||
});
|
||||
// Refresh every 30 seconds
|
||||
setTimeout(this.refreshPlayers, 30000);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<div className="section">
|
||||
<Table
|
||||
className="list players"
|
||||
columns={[
|
||||
{
|
||||
name: 'Name',
|
||||
render: function(userid) {
|
||||
var player = this.state.players[userid];
|
||||
return <a href={Link.get('player', userid)}>{ player.name }</a>;
|
||||
}.bind(this),
|
||||
sort: function(aid, bid) {
|
||||
var a = this.state.players[aid];
|
||||
var b = this.state.players[bid];
|
||||
return a.name.localeCompare(b.name);
|
||||
}.bind(this),
|
||||
},
|
||||
{
|
||||
name: 'Love',
|
||||
render: function(userid) {
|
||||
var player = this.state.players[userid];
|
||||
return player.love;
|
||||
}.bind(this),
|
||||
sort: function(aid, bid) {
|
||||
var a = this.state.players[aid];
|
||||
var b = this.state.players[bid];
|
||||
return a.love - b.love;
|
||||
}.bind(this),
|
||||
},
|
||||
{
|
||||
name: 'Level',
|
||||
render: function(userid) {
|
||||
var player = this.state.players[userid];
|
||||
return player.level;
|
||||
}.bind(this),
|
||||
sort: function(aid, bid) {
|
||||
var a = this.state.players[aid];
|
||||
var b = this.state.players[bid];
|
||||
return a.level - b.level;
|
||||
}.bind(this),
|
||||
},
|
||||
{
|
||||
name: 'Play Count',
|
||||
render: function(userid) {
|
||||
var player = this.state.players[userid];
|
||||
return player.plays;
|
||||
}.bind(this),
|
||||
sort: function(aid, bid) {
|
||||
var a = this.state.players[aid];
|
||||
var b = this.state.players[bid];
|
||||
return a.plays - b.plays;
|
||||
}.bind(this),
|
||||
reverse: true,
|
||||
},
|
||||
]}
|
||||
rows={Object.keys(this.state.players)}
|
||||
paginate={10}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(all_players, null),
|
||||
document.getElementById('content')
|
||||
);
|
115
bemani/frontend/static/controllers/hpnm/player.react.js
Normal file
115
bemani/frontend/static/controllers/hpnm/player.react.js
Normal file
@ -0,0 +1,115 @@
|
||||
/*** @jsx React.DOM */
|
||||
|
||||
var valid_versions = Object.keys(window.versions);
|
||||
var pagenav = new History(valid_versions);
|
||||
|
||||
var profile_view = createReactClass({
|
||||
|
||||
getInitialState: function(props) {
|
||||
var profiles = Object.keys(window.player);
|
||||
return {
|
||||
player: window.player,
|
||||
profiles: profiles,
|
||||
version: pagenav.getInitialState(profiles[profiles.length - 1]),
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
pagenav.onChange(function(version) {
|
||||
this.setState({version: version});
|
||||
}.bind(this));
|
||||
this.refreshProfile();
|
||||
},
|
||||
|
||||
refreshProfile: function() {
|
||||
AJAX.get(
|
||||
Link.get('refresh'),
|
||||
function(response) {
|
||||
var profiles = Object.keys(response.player);
|
||||
|
||||
this.setState({
|
||||
player: response.player,
|
||||
profiles: profiles,
|
||||
});
|
||||
setTimeout(this.refreshProfile, 5000);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.player[this.state.version]) {
|
||||
var player = this.state.player[this.state.version];
|
||||
return (
|
||||
<div>
|
||||
<div className="section popn-nav">
|
||||
<h3>{player.name}'s profile</h3>
|
||||
{this.state.profiles.map(function(version) {
|
||||
return (
|
||||
<Nav
|
||||
title={window.versions[version]}
|
||||
active={this.state.version == version}
|
||||
onClick={function(event) {
|
||||
if (this.state.version == version) { return; }
|
||||
this.setState({version: version});
|
||||
pagenav.navigate(version);
|
||||
}.bind(this)}
|
||||
/>
|
||||
);
|
||||
}.bind(this))}
|
||||
</div>
|
||||
<div className="section">
|
||||
<LabelledSection label="User ID">{player.extid}</LabelledSection>
|
||||
<LabelledSection label="Profile Created">
|
||||
<Timestamp timestamp={player.first_play_time}/>
|
||||
</LabelledSection>
|
||||
<LabelledSection label="Last Played">
|
||||
<Timestamp timestamp={player.last_play_time}/>
|
||||
</LabelledSection>
|
||||
<LabelledSection label="Total Rounds">
|
||||
{player.plays}回
|
||||
</LabelledSection>
|
||||
</div>
|
||||
<div className="section">
|
||||
<a href={Link.get('records')}>{ window.own_profile ?
|
||||
<span>view your records</span> :
|
||||
<span>view {player.name}'s records</span>
|
||||
}</a>
|
||||
<span className="separator">·</span>
|
||||
<a href={Link.get('scores')}>{ window.own_profile ?
|
||||
<span>view all your scores</span> :
|
||||
<span>view all {player.name}'s scores</span>
|
||||
}</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<div className="section">
|
||||
{this.state.profiles.map(function(version) {
|
||||
return (
|
||||
<Nav
|
||||
title={window.versions[version]}
|
||||
active={this.state.version == version}
|
||||
onClick={function(event) {
|
||||
if (this.state.version == version) { return; }
|
||||
this.setState({version: version});
|
||||
pagenav.navigate(version);
|
||||
}.bind(this)}
|
||||
/>
|
||||
);
|
||||
}.bind(this))}
|
||||
</div>
|
||||
<div className="section">
|
||||
This player has no profile for {window.versions[this.state.version]}!
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(profile_view, null),
|
||||
document.getElementById('content')
|
||||
);
|
546
bemani/frontend/static/controllers/hpnm/records.react.js
Normal file
546
bemani/frontend/static/controllers/hpnm/records.react.js
Normal file
@ -0,0 +1,546 @@
|
||||
/*** @jsx React.DOM */
|
||||
|
||||
var valid_sorts = ['series', 'name', 'popularity'];
|
||||
var valid_charts = ['Easy', 'Normal', 'Hard'];
|
||||
var valid_mixes = Object.keys(window.versions);
|
||||
var valid_subsorts = [valid_mixes, false, false, valid_charts, valid_charts];
|
||||
if (window.showpersonalsort) {
|
||||
valid_sorts.push('grade');
|
||||
valid_sorts.push('clear');
|
||||
}
|
||||
var pagenav = new History(valid_sorts, valid_subsorts);
|
||||
var sort_names = {
|
||||
'series': 'Series',
|
||||
'name': 'Song Name',
|
||||
'popularity': 'Popularity',
|
||||
'grade': 'Score',
|
||||
'clear': 'Status',
|
||||
};
|
||||
|
||||
var HighScore = createReactClass({
|
||||
render: function() {
|
||||
if (!this.props.score) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="score">
|
||||
<div>
|
||||
<span className="label">Score</span>
|
||||
<span className="score">{this.props.score.points}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="status">{this.convertClearType(this.props.score.clear_type)}</span>
|
||||
</div>
|
||||
{ this.props.score.userid && window.shownames ?
|
||||
<div><a href={Link.get('player', this.props.score.userid)}>{
|
||||
this.props.players[this.props.score.userid].name
|
||||
}</a></div> : null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
convertClearType: function (chart) {
|
||||
switch (chart) {
|
||||
case 1:
|
||||
return 'Failed...';
|
||||
case 2:
|
||||
return 'Clear!!';
|
||||
case 3:
|
||||
return 'Full Combo!!';
|
||||
case 4:
|
||||
return 'Perfect!';
|
||||
default:
|
||||
return 'unknown';
|
||||
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var network_records = createReactClass({
|
||||
|
||||
sortRecords: function(records) {
|
||||
var sorted_records = {};
|
||||
|
||||
records.forEach(function(record) {
|
||||
if (!(record.songid in sorted_records)) {
|
||||
sorted_records[record.songid] = {}
|
||||
}
|
||||
sorted_records[record.songid][record.chart] = record;
|
||||
});
|
||||
|
||||
return sorted_records;
|
||||
},
|
||||
|
||||
getInitialState: function(props) {
|
||||
return {
|
||||
songs: window.songs,
|
||||
records: this.sortRecords(window.records),
|
||||
players: window.players,
|
||||
versions: window.versions,
|
||||
sort: pagenav.getInitialState('series', '0'),
|
||||
subtab: this.getSubIndex('series', pagenav.getInitialSubState('series', '0')),
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
};
|
||||
},
|
||||
|
||||
getSubIndex: function(sort, subsort) {
|
||||
var subtab = 0;
|
||||
window.valid_sorts.forEach(function(potential, index) {
|
||||
if (window.valid_subsorts[index]) {
|
||||
window.valid_subsorts[index].forEach(function(subpotential, subindex) {
|
||||
if (subpotential == subsort) {
|
||||
subtab = subindex;
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
}.bind(this));
|
||||
return subtab;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
pagenav.onChange(function(sort, subsort) {
|
||||
var subtab = this.getSubIndex(sort, subsort);
|
||||
this.setState({sort: sort, offset: 0, subtab: subtab});
|
||||
}.bind(this));
|
||||
this.refreshRecords();
|
||||
},
|
||||
|
||||
refreshRecords: function() {
|
||||
AJAX.get(
|
||||
Link.get('refresh'),
|
||||
function(response) {
|
||||
this.setState({
|
||||
records: this.sortRecords(response.records),
|
||||
players: response.players,
|
||||
});
|
||||
// Refresh every 15 seconds
|
||||
setTimeout(this.refreshRecords, 15000);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
getPlays: function(record) {
|
||||
if (!record) { return 0; }
|
||||
var plays = 0;
|
||||
for (var i = 0; i < 4; i++) {
|
||||
if (record[i]) { plays += record[i].plays; }
|
||||
}
|
||||
return plays;
|
||||
},
|
||||
|
||||
renderBySeries: function() {
|
||||
|
||||
var songids = Object.keys(this.state.songs).sort(function(a, b) {
|
||||
var ac = this.state.songs[a].category;
|
||||
var bc = this.state.songs[b].category;
|
||||
return parseInt(bc) - parseInt(ac);
|
||||
}.bind(this));
|
||||
if (window.filterempty) {
|
||||
songids = songids.filter(function(songid) {
|
||||
return this.getPlays(this.state.records[songid]) > 0;
|
||||
}.bind(this));
|
||||
}
|
||||
var lastSeries = 0;
|
||||
for (var i = 0; i < songids.length; i++) {
|
||||
var curSeries = parseInt(this.state.songs[songids[i]].category) + 1;
|
||||
if (curSeries != lastSeries) {
|
||||
lastSeries = curSeries;
|
||||
songids.splice(i, 0, -curSeries);
|
||||
}
|
||||
}
|
||||
|
||||
if (songids.length == 0) {
|
||||
return (
|
||||
<div>
|
||||
No records to display!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var paginate = false;
|
||||
var curpage = -1;
|
||||
var curbutton = -1;
|
||||
if (songids.length > 99) {
|
||||
// Arbitrary limit for perf reasons
|
||||
paginate = true;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{ paginate ?
|
||||
<div className="section popn-nav" key="paginatebuttons">
|
||||
{songids.map(function(songid) {
|
||||
if (songid < 0) {
|
||||
curbutton = curbutton + 1;
|
||||
var subtab = curbutton;
|
||||
return (
|
||||
<Nav
|
||||
title={ this.state.versions[(-songid) - 1] }
|
||||
active={ subtab == this.state.subtab }
|
||||
onClick={function(event) {
|
||||
if (this.state.subtab == subtab) { return; }
|
||||
this.setState({subtab: subtab, offset: 0});
|
||||
pagenav.navigate(this.state.sort, window.valid_mixes[subtab]);
|
||||
}.bind(this)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}.bind(this))}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
<div className="section" key="contents">
|
||||
<table className="list records">
|
||||
<thead></thead>
|
||||
<tbody>
|
||||
{songids.map(function(songid) {
|
||||
console.log(songid)
|
||||
if (songid < 0) {
|
||||
// This is a series header
|
||||
curpage = curpage + 1;
|
||||
if (paginate && curpage != this.state.subtab) { return null; }
|
||||
|
||||
return (
|
||||
<tr key={songid.toString()} className="header">
|
||||
<td className="subheader">{
|
||||
!paginate ? this.state.versions[(-songid) - 1] : "Song / Artist / Difficulties"
|
||||
}</td>
|
||||
<td className="subheader">Easy</td>
|
||||
<td className="subheader">Normal</td>
|
||||
<td className="subheader">Hard</td>
|
||||
</tr>
|
||||
);
|
||||
} else {
|
||||
if (paginate && curpage != this.state.subtab) { return null; }
|
||||
|
||||
var records = this.state.records[songid];
|
||||
if (!records) {
|
||||
records = {};
|
||||
}
|
||||
|
||||
var difficulties = this.state.songs[songid].difficulties;
|
||||
return (
|
||||
<tr key={songid.toString()}>
|
||||
<td className="center">
|
||||
<div>
|
||||
<a href={Link.get('individual_score', songid)}>
|
||||
<div className="songname">{ this.state.songs[songid].name }</div>
|
||||
<div className="songartist">{ this.state.songs[songid].artist }</div>
|
||||
<div className="songgenre">{ this.state.songs[songid].genre }</div>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td className={difficulties[0] > 0 ? "" : "nochart"}>
|
||||
<HighScore
|
||||
players={this.state.players}
|
||||
songid={songid}
|
||||
chart={0}
|
||||
score={records[0]}
|
||||
/>
|
||||
</td>
|
||||
<td className={difficulties[1] > 0 ? "" : "nochart"}>
|
||||
<HighScore
|
||||
players={this.state.players}
|
||||
songid={songid}
|
||||
chart={1}
|
||||
score={records[1]}
|
||||
/>
|
||||
</td>
|
||||
<td className={difficulties[2] > 0 ? "" : "nochart"}>
|
||||
<HighScore
|
||||
players={this.state.players}
|
||||
songid={songid}
|
||||
chart={2}
|
||||
score={records[2]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}.bind(this))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
renderByName: function() {
|
||||
var songids = Object.keys(this.state.songs).sort(function(a, b) {
|
||||
var an = this.state.songs[a].name;
|
||||
var bn = this.state.songs[b].name;
|
||||
var c = an.localeCompare(bn);
|
||||
if (c == 0) {
|
||||
return parseInt(a) - parseInt(b)
|
||||
} else {
|
||||
return c;
|
||||
}
|
||||
}.bind(this));
|
||||
if (window.filterempty) {
|
||||
songids = songids.filter(function(songid) {
|
||||
return this.getPlays(this.state.records[songid]) > 0;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
return this.renderBySongIDList(songids, false);
|
||||
},
|
||||
|
||||
renderByPopularity: function() {
|
||||
var songids = Object.keys(this.state.songs).sort(function(a, b) {
|
||||
var ap = this.getPlays(this.state.records[a]);
|
||||
var bp = this.getPlays(this.state.records[b]);
|
||||
if (bp == ap) {
|
||||
return parseInt(a) - parseInt(b)
|
||||
} else {
|
||||
return bp - ap;
|
||||
}
|
||||
}.bind(this));
|
||||
if (window.filterempty) {
|
||||
songids = songids.filter(function(songid) {
|
||||
return this.getPlays(this.state.records[songid]) > 0;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
return this.renderBySongIDList(songids, true);
|
||||
},
|
||||
|
||||
renderByScore: function() {
|
||||
var songids = Object.keys(this.state.songs).sort(function(a, b) {
|
||||
// Grab records for this song
|
||||
var ar = this.state.records[a];
|
||||
var br = this.state.records[b];
|
||||
var ac = null;
|
||||
var bc = null;
|
||||
var as = 0;
|
||||
var bs = 0;
|
||||
|
||||
// Fill in record for current chart only if it exists
|
||||
if (ar) { ac = ar[this.state.subtab]; }
|
||||
if (br) { bc = br[this.state.subtab]; }
|
||||
if (ac) { as = ac.points; }
|
||||
if (bc) { bs = bc.points; }
|
||||
|
||||
if (bs == as) {
|
||||
return parseInt(a) - parseInt(b);
|
||||
} else {
|
||||
return bs - as;
|
||||
}
|
||||
}.bind(this));
|
||||
if (window.filterempty) {
|
||||
songids = songids.filter(function(songid) {
|
||||
return this.getPlays(this.state.records[songid]) > 0;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section">
|
||||
{window.valid_charts.map(function(chartname, index) {
|
||||
return (
|
||||
<Nav
|
||||
title={ chartname }
|
||||
active={ this.state.subtab == index }
|
||||
onClick={function(event) {
|
||||
if (this.state.subtab == index) { return; }
|
||||
this.setState({subtab: index, offset: 0});
|
||||
pagenav.navigate(this.state.sort, window.valid_charts[index]);
|
||||
}.bind(this)}
|
||||
/>
|
||||
);
|
||||
}.bind(this))}
|
||||
</div>
|
||||
{ this.renderBySongIDList(songids, false) }
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
renderByClearType: function() {
|
||||
var songids = Object.keys(this.state.songs).sort(function(a, b) {
|
||||
// Grab records for this song
|
||||
var ar = this.state.records[a];
|
||||
var br = this.state.records[b];
|
||||
var ac = null;
|
||||
var bc = null;
|
||||
var al = 0;
|
||||
var bl = 0;
|
||||
|
||||
// Fill in record for current chart only if it exists
|
||||
if (ar) { ac = ar[this.state.subtab]; }
|
||||
if (br) { bc = br[this.state.subtab]; }
|
||||
if (ac) { al = ac.clear_type; }
|
||||
if (bc) { bl = bc.clear_type; }
|
||||
|
||||
if (al == bl) {
|
||||
return parseInt(a) - parseInt(b)
|
||||
} else {
|
||||
return bl - al;
|
||||
}
|
||||
}.bind(this));
|
||||
if (window.filterempty) {
|
||||
songids = songids.filter(function(songid) {
|
||||
return this.getPlays(this.state.records[songid]) > 0;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="section">
|
||||
{window.valid_charts.map(function(chartname, index) {
|
||||
return (
|
||||
<Nav
|
||||
title={ chartname }
|
||||
active={ this.state.subtab == index }
|
||||
onClick={function(event) {
|
||||
if (this.state.subtab == index) { return; }
|
||||
this.setState({subtab: index, offset: 0});
|
||||
pagenav.navigate(this.state.sort, window.valid_charts[index]);
|
||||
}.bind(this)}
|
||||
/>
|
||||
);
|
||||
}.bind(this))}
|
||||
</div>
|
||||
{ this.renderBySongIDList(songids, false) }
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
renderBySongIDList: function(songids, showplays) {
|
||||
return (
|
||||
<div className="section">
|
||||
<table className="list records">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="subheader">Song / Artist / Difficulties</th>
|
||||
<th className="subheader">Easy</th>
|
||||
<th className="subheader">Normal</th>
|
||||
<th className="subheader">Hard</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{songids.map(function(songid, index) {
|
||||
if (index < this.state.offset || index >= this.state.offset + this.state.limit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var records = this.state.records[songid];
|
||||
if (!records) {
|
||||
records = {};
|
||||
}
|
||||
|
||||
var plays = this.getPlays(records);
|
||||
var difficulties = this.state.songs[songid].difficulties;
|
||||
return (
|
||||
<tr key={songid.toString()}>
|
||||
<td className="center">
|
||||
<div>
|
||||
<a href={Link.get('individual_score', songid)}>
|
||||
<div className="songname">{ this.state.songs[songid].name }</div>
|
||||
<div className="songartist">{ this.state.songs[songid].artist }</div>
|
||||
<div className="songgenre">{ this.state.songs[songid].genre }</div>
|
||||
</a>
|
||||
</div>
|
||||
{ showplays ? <div className="songplays">#{index + 1} - {plays}{plays == 1 ? ' play' : ' plays'}</div> : null }
|
||||
</td>
|
||||
<td className={difficulties[0] > 0 ? "" : "nochart"}>
|
||||
<HighScore
|
||||
players={this.state.players}
|
||||
songid={songid}
|
||||
chart={0}
|
||||
score={records[0]}
|
||||
/>
|
||||
</td>
|
||||
<td className={difficulties[1] > 0 ? "" : "nochart"}>
|
||||
<HighScore
|
||||
players={this.state.players}
|
||||
songid={songid}
|
||||
chart={1}
|
||||
score={records[1]}
|
||||
/>
|
||||
</td>
|
||||
<td className={difficulties[2] > 0 ? "" : "nochart"}>
|
||||
<HighScore
|
||||
players={this.state.players}
|
||||
songid={songid}
|
||||
chart={2}
|
||||
score={records[2]}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}.bind(this))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={5}>
|
||||
{ this.state.offset > 0 ?
|
||||
<Prev onClick={function(event) {
|
||||
var page = this.state.offset - this.state.limit;
|
||||
if (page < 0) { page = 0; }
|
||||
this.setState({offset: page});
|
||||
}.bind(this)}/> : null
|
||||
}
|
||||
{ (this.state.offset + this.state.limit) < songids.length ?
|
||||
<Next style={ {float: 'right'} } onClick={function(event) {
|
||||
var page = this.state.offset + this.state.limit;
|
||||
if (page >= songids.length) { return }
|
||||
this.setState({offset: page});
|
||||
}.bind(this)}/> :
|
||||
null
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var data = null;
|
||||
if (this.state.sort == 'series') {
|
||||
data = this.renderBySeries();
|
||||
} else if (this.state.sort == 'popularity') {
|
||||
data = this.renderByPopularity();
|
||||
} else if (this.state.sort == 'name') {
|
||||
data = this.renderByName();
|
||||
} else if (this.state.sort == 'grade') {
|
||||
data = this.renderByScore();
|
||||
} else if (this.state.sort == 'clear') {
|
||||
data = this.renderByClearType();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="section">
|
||||
{ window.valid_sorts.map(function(sort, index) {
|
||||
return (
|
||||
<Nav
|
||||
title={"Records Sorted by " + window.sort_names[sort]}
|
||||
active={this.state.sort == sort}
|
||||
onClick={function(event) {
|
||||
if (this.state.sort == sort) { return; }
|
||||
this.setState({sort: sort, offset: 0, subtab: 0});
|
||||
pagenav.navigate(sort, window.valid_subsorts[index][0]);
|
||||
}.bind(this)}
|
||||
/>
|
||||
);
|
||||
}.bind(this)) }
|
||||
</div>
|
||||
<div className="section">
|
||||
{data}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(network_records, null),
|
||||
document.getElementById('content')
|
||||
);
|
160
bemani/frontend/static/controllers/hpnm/scores.react.js
Normal file
160
bemani/frontend/static/controllers/hpnm/scores.react.js
Normal file
@ -0,0 +1,160 @@
|
||||
/*** @jsx React.DOM */
|
||||
|
||||
|
||||
var network_scores = createReactClass({
|
||||
|
||||
getInitialState: function (props) {
|
||||
|
||||
return {
|
||||
songs: window.songs,
|
||||
attempts: window.attempts,
|
||||
players: window.players,
|
||||
loading: true,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
open: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function () {
|
||||
this.refreshScores();
|
||||
},
|
||||
|
||||
refreshScores: function () {
|
||||
AJAX.get(
|
||||
Link.get('refresh'),
|
||||
function (response) {
|
||||
this.setState({
|
||||
attempts: response.attempts,
|
||||
players: response.players,
|
||||
loading: false,
|
||||
});
|
||||
// Refresh every 15 seconds
|
||||
setTimeout(this.refreshScores, 15000);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
convertChart: function (chart) {
|
||||
switch (chart) {
|
||||
case 0:
|
||||
return 'Easy';
|
||||
case 1:
|
||||
return 'Normal';
|
||||
case 2:
|
||||
return 'Hard';
|
||||
default:
|
||||
return 'u broke it';
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
convertClearType: function (chart) {
|
||||
switch (chart) {
|
||||
case 1:
|
||||
return 'Failed...';
|
||||
case 2:
|
||||
return 'Clear!!';
|
||||
case 3:
|
||||
return 'Full Combo!!';
|
||||
case 4:
|
||||
return 'Perfect!';
|
||||
default:
|
||||
return 'unknown';
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
render: function () {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className="list attempts">
|
||||
<thead>
|
||||
<tr>
|
||||
{window.shownames ? <th>Name</th> : null}
|
||||
<th>Timestamp</th>
|
||||
<th>Song / Artist</th>
|
||||
<th>Difficulty</th>
|
||||
<th>Result</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.state.attempts.map(function (attempt, index) {
|
||||
if (index < this.state.offset || index >= this.state.offset + this.state.limit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
{window.shownames ? <td><a href={Link.get('player', attempt.userid)}>{
|
||||
this.state.players[attempt.userid].name
|
||||
}</a></td> : null}
|
||||
<td>
|
||||
<div>
|
||||
<Timestamp timestamp={attempt.timestamp} />
|
||||
{window.shownewrecords && attempt.raised ?
|
||||
<span className="raised">new high score!</span> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td className="center">
|
||||
<a href={Link.get('individual_score', attempt.songid)}>
|
||||
<div className="songname">{this.state.songs[attempt.songid].name}</div>
|
||||
<div className="songartist">{this.state.songs[attempt.songid].artist}</div>
|
||||
</a>
|
||||
</td>
|
||||
<td className="center">
|
||||
<div>
|
||||
{this.convertChart(attempt.chart)}
|
||||
</div>
|
||||
|
||||
</td>
|
||||
<td className="center">{this.convertClearType(attempt.clear_type)}</td>
|
||||
<td className="center">{attempt.points}</td>
|
||||
</tr>
|
||||
);
|
||||
}.bind(this))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={7}>
|
||||
{this.state.offset > 0 ?
|
||||
<Prev onClick={function (event) {
|
||||
var page = this.state.offset - this.state.limit;
|
||||
if (page < 0) { page = 0; }
|
||||
this.setState({ offset: page });
|
||||
}.bind(this)} /> : null
|
||||
}
|
||||
{(this.state.offset + this.state.limit) < this.state.attempts.length ?
|
||||
<Next style={{ float: 'right' }} onClick={function (event) {
|
||||
var page = this.state.offset + this.state.limit;
|
||||
if (page >= this.state.attempts.length) { return }
|
||||
this.setState({ offset: page });
|
||||
}.bind(this)} /> :
|
||||
this.state.loading ?
|
||||
<span className="loading" style={{ float: 'right' }}>
|
||||
<img
|
||||
className="loading"
|
||||
src={Link.get('static', window.assets + 'loading-16.gif')}
|
||||
/> loading more scores...
|
||||
</span> : null
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(network_scores, null),
|
||||
document.getElementById('content')
|
||||
);
|
194
bemani/frontend/static/controllers/hpnm/settings.react.js
Normal file
194
bemani/frontend/static/controllers/hpnm/settings.react.js
Normal file
@ -0,0 +1,194 @@
|
||||
/*** @jsx React.DOM */
|
||||
|
||||
var valid_versions = Object.keys(window.versions);
|
||||
var pagenav = new History(valid_versions);
|
||||
|
||||
var settings_view = createReactClass({
|
||||
|
||||
getInitialState: function(props) {
|
||||
var profiles = Object.keys(window.player);
|
||||
var version = pagenav.getInitialState(profiles[profiles.length - 1]);
|
||||
return {
|
||||
player: window.player,
|
||||
profiles: profiles,
|
||||
version: version,
|
||||
new_name: window.player[version].name,
|
||||
editing_name: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
pagenav.onChange(function(version) {
|
||||
this.setState({version: version});
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
if (this.focus_element && this.focus_element != this.already_focused) {
|
||||
this.focus_element.focus();
|
||||
this.already_focused = this.focus_element;
|
||||
}
|
||||
},
|
||||
|
||||
saveName: function(event) {
|
||||
AJAX.post(
|
||||
Link.get('updatename'),
|
||||
{
|
||||
version: this.state.version,
|
||||
name: this.state.new_name,
|
||||
},
|
||||
function(response) {
|
||||
var player = this.state.player;
|
||||
player[response.version].name = response.name;
|
||||
this.setState({
|
||||
player: player,
|
||||
new_name: this.state.player[response.version].name,
|
||||
editing_name: false,
|
||||
});
|
||||
}.bind(this)
|
||||
);
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
renderName: function(player) {
|
||||
return (
|
||||
<LabelledSection vertical={true} label="Name">{
|
||||
!this.state.editing_name ?
|
||||
<>
|
||||
<span>{player.name}</span>
|
||||
<Edit
|
||||
onClick={function(event) {
|
||||
this.setState({editing_name: true});
|
||||
}.bind(this)}
|
||||
/>
|
||||
</> :
|
||||
<form className="inline" onSubmit={this.saveName}>
|
||||
<input
|
||||
type="text"
|
||||
className="inline"
|
||||
maxlength="6"
|
||||
size="12"
|
||||
autofocus="true"
|
||||
ref={c => (this.focus_element = c)}
|
||||
value={this.state.new_name}
|
||||
onChange={function(event) {
|
||||
var rawvalue = event.target.value;
|
||||
var value = "";
|
||||
// Nasty conversion to change typing into wide text
|
||||
for (var i = 0; i < rawvalue.length; i++) {
|
||||
var c = rawvalue.charCodeAt(i);
|
||||
if (c >= '0'.charCodeAt(0) && c <= '9'.charCodeAt(0)) {
|
||||
c = 0xFF10 + (c - '0'.charCodeAt(0));
|
||||
} else if(c >= 'A'.charCodeAt(0) && c <= 'Z'.charCodeAt(0)) {
|
||||
c = 0xFF21 + (c - 'A'.charCodeAt(0));
|
||||
} else if(c >= 'a'.charCodeAt(0) && c <= 'z'.charCodeAt(0)) {
|
||||
c = 0xFF41 + (c - 'a'.charCodeAt(0));
|
||||
} else if(c == '@'.charCodeAt(0)) {
|
||||
c = 0xFF20;
|
||||
} else if(c == ','.charCodeAt(0)) {
|
||||
c = 0xFF0C;
|
||||
} else if(c == '.'.charCodeAt(0)) {
|
||||
c = 0xFF0E;
|
||||
} else if(c == '_'.charCodeAt(0)) {
|
||||
c = 0xFF3F;
|
||||
}
|
||||
value = value + String.fromCharCode(c);
|
||||
}
|
||||
var nameRegex = new RegExp(
|
||||
"^[" +
|
||||
"\uFF20-\uFF3A" + // widetext A-Z and @
|
||||
"\uFF41-\uFF5A" + // widetext a-z
|
||||
"\uFF10-\uFF19" + // widetext 0-9
|
||||
"\uFF0C\uFF0E\uFF3F" + // widetext ,._
|
||||
"\u3041-\u308D\u308F\u3092\u3093" + // hiragana
|
||||
"\u30A1-\u30ED\u30EF\u30F2\u30F3\u30FC" + // katakana
|
||||
"]*$"
|
||||
);
|
||||
if (value.length <= 6 && nameRegex.test(value)) {
|
||||
this.setState({new_name: value});
|
||||
}
|
||||
}.bind(this)}
|
||||
name="name"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
value="save"
|
||||
/>
|
||||
<input
|
||||
type="button"
|
||||
value="cancel"
|
||||
onClick={function(event) {
|
||||
this.setState({
|
||||
new_name: this.state.player[this.state.version].name,
|
||||
editing_name: false,
|
||||
});
|
||||
}.bind(this)}
|
||||
/>
|
||||
</form>
|
||||
}</LabelledSection>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.player[this.state.version]) {
|
||||
var player = this.state.player[this.state.version];
|
||||
return (
|
||||
<div>
|
||||
<div className="section popn-nav">
|
||||
{this.state.profiles.map(function(version) {
|
||||
return (
|
||||
<Nav
|
||||
title={window.versions[version]}
|
||||
active={this.state.version == version}
|
||||
onClick={function(event) {
|
||||
if (this.state.editing_name) { return; }
|
||||
if (this.state.version == version) { return; }
|
||||
this.setState({
|
||||
version: version,
|
||||
new_name: this.state.player[version].name,
|
||||
});
|
||||
pagenav.navigate(version);
|
||||
}.bind(this)}
|
||||
/>
|
||||
);
|
||||
}.bind(this))}
|
||||
</div>
|
||||
<div className="section">
|
||||
<h3>User Profile</h3>
|
||||
{this.renderName(player)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<div className="section">
|
||||
You have no profile for {window.versions[this.state.version]}!
|
||||
</div>
|
||||
<div className="section">
|
||||
{this.state.profiles.map(function(version) {
|
||||
return (
|
||||
<Nav
|
||||
title={window.versions[version]}
|
||||
active={this.state.version == version}
|
||||
onClick={function(event) {
|
||||
if (this.state.version == version) { return; }
|
||||
this.setState({
|
||||
version: version,
|
||||
});
|
||||
pagenav.navigate(version);
|
||||
}.bind(this)}
|
||||
/>
|
||||
);
|
||||
}.bind(this))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(settings_view, null),
|
||||
document.getElementById('content')
|
||||
);
|
150
bemani/frontend/static/controllers/hpnm/topscores.react.js
Normal file
150
bemani/frontend/static/controllers/hpnm/topscores.react.js
Normal file
@ -0,0 +1,150 @@
|
||||
/*** @jsx React.DOM */
|
||||
|
||||
var valid_charts = ['Easy', 'Normal', 'Hard']
|
||||
var pagenav = new History(valid_charts);
|
||||
|
||||
var top_scores = createReactClass({
|
||||
|
||||
sortTopScores: function(topscores) {
|
||||
var newscores = [[], [], [], []];
|
||||
topscores.map(function(score) {
|
||||
newscores[score.chart].push(score);
|
||||
}.bind(this));
|
||||
return newscores;
|
||||
},
|
||||
|
||||
getInitialState: function(props) {
|
||||
return {
|
||||
topscores: this.sortTopScores(window.topscores),
|
||||
players: window.players,
|
||||
chart: pagenav.getInitialState(valid_charts[0]),
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
pagenav.onChange(function(chart) {
|
||||
this.setState({chart: chart});
|
||||
}.bind(this));
|
||||
this.refreshScores();
|
||||
},
|
||||
|
||||
refreshScores: function() {
|
||||
AJAX.get(
|
||||
Link.get('refresh'),
|
||||
function(response) {
|
||||
this.setState({
|
||||
topscores: this.sortTopScores(response.topscores),
|
||||
players: response.players,
|
||||
});
|
||||
// Refresh every 15 seconds
|
||||
setTimeout(this.refreshScores, 15000);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
convertChart: function(chart) {
|
||||
switch(chart) {
|
||||
case 'Easy':
|
||||
return 0;
|
||||
case 'Normal':
|
||||
return 1;
|
||||
case 'Hard':
|
||||
return 2;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
render: function() {
|
||||
var chart = this.convertChart(this.state.chart);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="section">
|
||||
<div className="songname">{window.name}</div>
|
||||
<div className="songartist">{window.artist}</div>
|
||||
</div>
|
||||
<div className="section">
|
||||
{valid_charts.map(function(chart) {
|
||||
return (
|
||||
<Nav
|
||||
title={chart}
|
||||
active={this.state.chart == chart}
|
||||
onClick={function(event) {
|
||||
if (this.state.chart == chart) { return; }
|
||||
this.setState({chart: chart});
|
||||
pagenav.navigate(chart);
|
||||
}.bind(this)}
|
||||
/>
|
||||
);
|
||||
}.bind(this))}
|
||||
</div>
|
||||
<div className="section">
|
||||
<Table
|
||||
className="list topscores"
|
||||
columns={[
|
||||
{
|
||||
name: 'Name',
|
||||
render: function(topscore) {
|
||||
return (
|
||||
<a href={Link.get('player', topscore.userid)}>{
|
||||
this.state.players[topscore.userid].name
|
||||
}</a>
|
||||
);
|
||||
}.bind(this),
|
||||
sort: function(a, b) {
|
||||
var an = this.state.players[a.userid].name;
|
||||
var bn = this.state.players[b.userid].name;
|
||||
return an.localeCompare(bn);
|
||||
}.bind(this),
|
||||
},
|
||||
{
|
||||
name: 'Result',
|
||||
render: function(topscore) { return this.convertClearType(topscore.clear_type); },
|
||||
sort: function(a, b) {
|
||||
return a.clear_type - b.clear_type;
|
||||
},
|
||||
reverse: true,
|
||||
convertClearType: function (chart) {
|
||||
switch (chart) {
|
||||
case 1:
|
||||
return 'Failed...';
|
||||
case 2:
|
||||
return 'Clear!!';
|
||||
case 3:
|
||||
return 'Full Combo!!';
|
||||
case 4:
|
||||
return 'Perfect!';
|
||||
default:
|
||||
return 'unknown';
|
||||
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Score',
|
||||
render: function(topscore) { return topscore.points; },
|
||||
sort: function(a, b) {
|
||||
return a.points - b.points;
|
||||
},
|
||||
reverse: true,
|
||||
},
|
||||
]}
|
||||
defaultsort='Score'
|
||||
rows={this.state.topscores[chart]}
|
||||
key={chart}
|
||||
paginate={10}
|
||||
emptymessage="There are no scores for this chart."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(top_scores, null),
|
||||
document.getElementById('content')
|
||||
);
|
@ -11,10 +11,12 @@ from bemani.backend.sdvx import SoundVoltexFactory
|
||||
from bemani.backend.reflec import ReflecBeatFactory
|
||||
from bemani.backend.museca import MusecaFactory
|
||||
from bemani.backend.mga import MetalGearArcadeFactory
|
||||
from bemani.backend.hellopopn import HelloPopnFactory
|
||||
from bemani.common import GameConstants, cache
|
||||
from bemani.data import Config, Data
|
||||
|
||||
|
||||
|
||||
def load_config(filename: str, config: Config) -> None:
|
||||
config.update(yaml.safe_load(open(filename)))
|
||||
config["database"]["engine"] = Data.create_engine(config)
|
||||
@ -78,3 +80,5 @@ def register_games(config: Config) -> None:
|
||||
MusecaFactory.register_all()
|
||||
if GameConstants.MGA in config.support:
|
||||
MetalGearArcadeFactory.register_all()
|
||||
if GameConstants.HELLO_POPN in config.support:
|
||||
HelloPopnFactory.register_all()
|
||||
|
@ -16,6 +16,7 @@ from bemani.frontend.ddr import ddr_pages
|
||||
from bemani.frontend.sdvx import sdvx_pages
|
||||
from bemani.frontend.reflec import reflec_pages
|
||||
from bemani.frontend.museca import museca_pages
|
||||
from bemani.frontend.hellopopn import hpnm_pages
|
||||
from bemani.utils.config import (
|
||||
load_config as base_load_config,
|
||||
instantiate_cache as base_instantiate_cache,
|
||||
@ -49,6 +50,8 @@ def register_blueprints() -> None:
|
||||
app.register_blueprint(reflec_pages)
|
||||
if GameConstants.MUSECA in config.support:
|
||||
app.register_blueprint(museca_pages)
|
||||
if GameConstants.HELLO_POPN in config.support:
|
||||
app.register_blueprint(hpnm_pages)
|
||||
|
||||
|
||||
def register_games() -> None:
|
||||
|
@ -4506,6 +4506,154 @@ class ImportMuseca(ImportBase):
|
||||
self.finish_batch()
|
||||
|
||||
|
||||
class ImportHelloPopn(ImportBase):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Dict[str, Any],
|
||||
version: str,
|
||||
no_combine: bool,
|
||||
update: bool,
|
||||
) -> None:
|
||||
if version in ['1']:
|
||||
actual_version = {
|
||||
'1': VersionConstants.HELLO_POPN_MUSIC,
|
||||
}.get(version, -1)
|
||||
elif actual_version == 'all':
|
||||
actual_version = None
|
||||
|
||||
if actual_version in [
|
||||
None,
|
||||
VersionConstants.HELLO_POPN_MUSIC,
|
||||
]:
|
||||
self.charts = [0, 1, 2]
|
||||
|
||||
else:
|
||||
raise Exception("Unsupported Hello Pop'n Music version! Please use one of the following: hellopopn.")
|
||||
|
||||
super().__init__(config, GameConstants.HELLO_POPN, actual_version, no_combine, update)
|
||||
|
||||
def scrape(self, tsvfile: str) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
||||
if self.version is None:
|
||||
raise Exception('Can\'t scrape database for \'all\' version!')
|
||||
songs = []
|
||||
with open(tsvfile, newline='', encoding="utf-8") as tsvhandle:
|
||||
beatread = csv.reader(tsvhandle, delimiter='|', quotechar='"') # type: ignore
|
||||
for row in beatread:
|
||||
songid = int(row[0])
|
||||
name = row[1]
|
||||
artist = row[2]
|
||||
easy = 1
|
||||
normal = 10
|
||||
hard = 100
|
||||
genre = ""
|
||||
|
||||
song = {
|
||||
'id': songid,
|
||||
'title': name,
|
||||
'artist': artist,
|
||||
'genre': genre,
|
||||
'difficulty': {
|
||||
'easy': easy,
|
||||
'normal': normal,
|
||||
'hard': hard,
|
||||
},
|
||||
'category': self.version
|
||||
}
|
||||
songs.append(song)
|
||||
|
||||
return songs
|
||||
|
||||
def lookup(self, server: str, token: str) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
|
||||
if self.version is None:
|
||||
raise Exception('Can\'t look up database for \'all\' version!')
|
||||
|
||||
# Grab music info from remote server
|
||||
music = self.remote_music(server, token)
|
||||
songs = music.get_all_songs(self.game, self.version)
|
||||
lut: Dict[int, Dict[str, Any]] = {}
|
||||
chart_map = {
|
||||
0: 'easy',
|
||||
1: 'normal',
|
||||
2: 'hard',
|
||||
}
|
||||
|
||||
# Format it the way we expect
|
||||
for song in songs:
|
||||
if song.chart not in chart_map:
|
||||
# Ignore charts on songs we don't support/care about.
|
||||
continue
|
||||
|
||||
if song.id not in lut:
|
||||
lut[song.id] = {
|
||||
'id': song.id,
|
||||
'title': song.name,
|
||||
'artist': song.artist,
|
||||
'genre': song.genre,
|
||||
'difficulty': {
|
||||
'easy': 0,
|
||||
'normal': 1,
|
||||
'hard': 2,
|
||||
},
|
||||
'category': self.version
|
||||
}
|
||||
lut[song.id]['difficulty'][chart_map[song.chart]] = song.data.get_int('difficulty')
|
||||
|
||||
# Reassemble the data
|
||||
reassembled_songs = [val for _, val in lut.items()]
|
||||
|
||||
|
||||
return reassembled_songs
|
||||
|
||||
def import_music_db(self, songs: List[Dict[str, Any]],version: int) -> None:
|
||||
if self.version is None:
|
||||
raise Exception('Can\'t import database for \'all\' version!')
|
||||
|
||||
chart_map: Dict[int, str] = {
|
||||
0: 'easy',
|
||||
1: 'normal',
|
||||
2: 'hard',
|
||||
}
|
||||
for song in songs:
|
||||
songid = song['id']
|
||||
|
||||
self.start_batch()
|
||||
for chart in self.charts:
|
||||
# First, try to find in the DB from another version
|
||||
old_id = self.get_music_id_for_song(songid, chart, version)
|
||||
|
||||
if(chart <= 2):
|
||||
# First, try to find in the DB from another version
|
||||
if self.no_combine is None or old_id is None:
|
||||
# Insert original
|
||||
print(f"New entry for {songid} chart {chart}")
|
||||
next_id = self.get_next_music_id()
|
||||
else:
|
||||
# Insert pointing at same ID so scores transfer
|
||||
print(f"Reused entry for {songid} chart {chart}")
|
||||
next_id = self.get_next_music_id()
|
||||
data = {
|
||||
'difficulty': song['difficulty'][chart_map[chart]],
|
||||
'category': self.version
|
||||
}
|
||||
else:
|
||||
# First, try to find in the DB from another version
|
||||
if self.no_combine is None:
|
||||
# Insert original
|
||||
print(f"New entry for {songid} chart {chart}")
|
||||
next_id = self.get_next_music_id()
|
||||
else:
|
||||
# Insert pointing at same ID so scores transfer
|
||||
print(f"Reused entry for {songid} chart {chart}")
|
||||
next_id = self.get_next_music_id()
|
||||
data = {
|
||||
'difficulty': song['difficulty'][chart_map[chart]],
|
||||
'category': self.version
|
||||
}
|
||||
self.insert_music_id_for_song(next_id, songid, chart, song['title'], song['artist'], song['genre'], data)
|
||||
self.finish_batch()
|
||||
|
||||
|
||||
class ReflecBeatScrapeConfiguration:
|
||||
def __init__(
|
||||
self,
|
||||
@ -5231,6 +5379,21 @@ def main() -> None:
|
||||
danevo.import_music_db(songs)
|
||||
danevo.close()
|
||||
|
||||
elif series == GameConstants.HELLO_POPN:
|
||||
hellopopn = ImportHelloPopn(config, args.version, args.no_combine, args.update)
|
||||
# Normal case, doing a music DB or emblem import.
|
||||
if args.tsv is not None:
|
||||
songs = hellopopn.scrape(args.tsv)
|
||||
elif args.server and args.token:
|
||||
songs = hellopopn.lookup(args.server, args.token)
|
||||
else:
|
||||
raise Exception(
|
||||
'No TSV provided and no remote server specified! Please ' +
|
||||
'provide either a --tsv or a --server and --token option!'
|
||||
)
|
||||
hellopopn.import_music_db(songs,args.version)
|
||||
hellopopn.close()
|
||||
|
||||
else:
|
||||
raise CLIException("Unsupported game series!")
|
||||
|
||||
|
@ -10,6 +10,7 @@ from bemani.backend.ddr import DDRFactory
|
||||
from bemani.backend.sdvx import SoundVoltexFactory
|
||||
from bemani.backend.reflec import ReflecBeatFactory
|
||||
from bemani.backend.museca import MusecaFactory
|
||||
from bemani.backend.hellopopn import HelloPopnFactory
|
||||
from bemani.frontend.popn import PopnMusicCache
|
||||
from bemani.frontend.iidx import IIDXCache
|
||||
from bemani.frontend.jubeat import JubeatCache
|
||||
@ -19,6 +20,7 @@ from bemani.frontend.ddr import DDRCache
|
||||
from bemani.frontend.sdvx import SoundVoltexCache
|
||||
from bemani.frontend.reflec import ReflecBeatCache
|
||||
from bemani.frontend.museca import MusecaCache
|
||||
from bemani.frontend.hellopopn import HelloPopnMusicCache
|
||||
from bemani.common import GameConstants, Time
|
||||
from bemani.data import Config, Data
|
||||
from bemani.utils.config import load_config, instantiate_cache
|
||||
@ -57,6 +59,9 @@ def run_scheduled_work(config: Config) -> None:
|
||||
if GameConstants.MUSECA in config.support:
|
||||
enabled_factories.append(MusecaFactory)
|
||||
enabled_caches.append(MusecaCache)
|
||||
if GameConstants.HELLO_POPN in config.support:
|
||||
enabled_factories.append(HelloPopnFactory)
|
||||
enabled_caches.append(HelloPopnMusicCache)
|
||||
|
||||
# First, run any backend scheduled work
|
||||
for factory in enabled_factories:
|
||||
|
@ -59,6 +59,7 @@ from bemani.client.reflec import (
|
||||
)
|
||||
from bemani.client.bishi import TheStarBishiBashiClient
|
||||
from bemani.client.mga.mga import MetalGearArcadeClient
|
||||
from bemani.client.hellopopn import HelloPopnMuiscClient
|
||||
|
||||
|
||||
def get_client(proto: ClientProtocol, pcbid: str, game: str, config: Dict[str, Any]) -> BaseClient:
|
||||
@ -308,6 +309,12 @@ def get_client(proto: ClientProtocol, pcbid: str, game: str, config: Dict[str, A
|
||||
pcbid,
|
||||
config,
|
||||
)
|
||||
if game == 'hpnm':
|
||||
return HelloPopnMuiscClient(
|
||||
proto,
|
||||
pcbid,
|
||||
config,
|
||||
)
|
||||
|
||||
raise Exception(f"Unknown game {game}")
|
||||
|
||||
@ -535,6 +542,11 @@ def mainloop(
|
||||
"model": "I36:J:A:A:2011092900",
|
||||
"avs": None,
|
||||
},
|
||||
'hpnm': {
|
||||
'name': "Hello Pop'n Music",
|
||||
'model': "JMP:J:A:A:2014122500",
|
||||
'avs': None,
|
||||
},
|
||||
}
|
||||
if action == "list":
|
||||
for game in sorted([game for game in games]):
|
||||
@ -656,6 +668,7 @@ def main() -> None:
|
||||
"reflec-5": "reflec-volzza",
|
||||
"reflec-6": "reflec-volzza2",
|
||||
"mga": "metal-gear-arcade",
|
||||
'hpnm': 'hpnm',
|
||||
}.get(game, game)
|
||||
|
||||
mainloop(args.address, args.port, args.config, action, game, args.cardid, args.verbose)
|
||||
|
@ -56,3 +56,6 @@ set -e
|
||||
./read --series reflec --version 4 "$@"
|
||||
./read --series reflec --version 5 "$@"
|
||||
./read --series reflec --version 6 "$@"
|
||||
|
||||
# Init Hello Pop'n Music
|
||||
./read --series hpnm --version 1 "$@"
|
||||
|
@ -87,6 +87,8 @@ support:
|
||||
reflec: True
|
||||
# SDVX frontend/backend enabled.
|
||||
sdvx: True
|
||||
# Hello Pop'n Music frontend/backend enabled
|
||||
hpnm: True
|
||||
|
||||
# Key used to encrypt cookies, should be unique per network instance.
|
||||
secret_key: 'this_is_a_secret_please_change_me'
|
||||
|
36
data/hellopopn.tsv
Normal file
36
data/hellopopn.tsv
Normal file
@ -0,0 +1,36 @@
|
||||
0|崖の上のポニョ|井上あずみ
|
||||
1|Dragon Soul|谷本貴義
|
||||
2|Blow Me Up|Sota Fujimori feat. Calin
|
||||
3|Cloudy Skies|EGOISTIC LEMONTEA
|
||||
4|男々道|Des-ROW・組
|
||||
5|Fantasia|TЁЯRA
|
||||
6|fffff|Five Hammer
|
||||
7|High School Love|DJ YOSHITAKA feat.DWP
|
||||
8|Homesick Pt.2&3|ORANGENOISE SHORTCUT
|
||||
9|SA-DA-ME|good-cool feat.すわひでお
|
||||
10|♥LOVE²シュガ→♥|dj TAKA feat.のりあ
|
||||
11|ナタラディーン|Q-Mex
|
||||
12|にゃんだふる55 marble version|Dormir
|
||||
13|Pink Rose|Kiyommy+Seiya
|
||||
14|ポップミュージック論|ギラギラメガネ団
|
||||
15|凛として咲く花の如く|紅色リトマス
|
||||
16|サヨナラ・ヘヴン|猫叉Master
|
||||
17|Soul On Fire|L.E.D.-G VS GUHROOVY fw NO+CHIN
|
||||
18|Übertreffen|TAKA respect for J.S.B.
|
||||
19|うるとらボーイ|V.C.O. featuring Yuko Asai
|
||||
20|Blind Justice ~Torn souls, Hurt Faiths~|Zektbach
|
||||
21|キセキ|GReeeeN
|
||||
22|じょいふる|いきものがかり
|
||||
23|ヘビーローテーション|AKB48
|
||||
24|BABY P|Plus-Tech Squeeze Box
|
||||
25|チェイス!チェイス!チェイス!|パーキッツ
|
||||
26|ふしぎなくすり|上野圭一 feat. SATOE
|
||||
27|隅田川夏恋歌|seiya-murai feat.ALT
|
||||
28|smooooch・∀・|kors k
|
||||
29|DROP THE BOMB|Scotty D.
|
||||
30|First Date|ko-saku
|
||||
31|jewelry girl*|jun
|
||||
32|天庭 おとこのこ編|あさき
|
||||
33|murmur twins|yu_tokiwa.djw
|
||||
34|恐怖の右脳改革|96
|
||||
35|LOVE♥SHINE|小坂りゆ
|
Can't render this file because it has a wrong number of fields in line 4.
|
@ -44,6 +44,7 @@ declare -a arr=(
|
||||
"reflec-5"
|
||||
"reflec-6"
|
||||
"mga"
|
||||
"hpnm"
|
||||
)
|
||||
|
||||
for project in "${arr[@]}"
|
||||
|
Loading…
x
Reference in New Issue
Block a user