1
0
mirror of synced 2025-02-17 11:18:33 +01:00

Hello Pop'n Music support

This commit is contained in:
Shinrin Ouja Moriking 2024-03-29 17:46:33 -06:00
parent 223c93874c
commit 2b6ce21084
31 changed files with 2522 additions and 3 deletions

View File

@ -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

View File

@ -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,

View File

@ -0,0 +1,7 @@
from bemani.backend.hellopopn.factory import HelloPopnFactory
from bemani.backend.hellopopn.base import HelloPopnBase
__all__ = [
"HelloPopnFactory",
"HelloPopnBase",
]

View 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

View 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

View 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

View File

@ -0,0 +1,6 @@
from bemani.client.hellopopn.hellopopn import HelloPopnMuiscClient
__all__ = [
"HelloPopnMuiscClient",
]

View 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 = ""
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)

View File

@ -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):
"""

View File

@ -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,

View File

@ -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(

View File

@ -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 = [

View File

@ -0,0 +1,8 @@
from bemani.frontend.hellopopn.endpoints import hpnm_pages
from bemani.frontend.hellopopn.cache import HelloPopnMusicCache
__all__ = [
"HelloPopnMusicCache",
"hpnm_pages",
]

View 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)

View 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": {},
}

View 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

View 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')
);

View 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">&middot;</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')
);

View 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')
);

View 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')
);

View 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')
);

View 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')
);

View File

@ -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()

View File

@ -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:

View File

@ -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!")

View File

@ -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:

View File

@ -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)

View File

@ -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 "$@"

View File

@ -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
View 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.

View File

@ -44,6 +44,7 @@ declare -a arr=(
"reflec-5"
"reflec-6"
"mga"
"hpnm"
)
for project in "${arr[@]}"