Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hello Pop'n Music support #87

Open
wants to merge 3 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.assets
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions bemani/backend/hellopopn/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from bemani.backend.hellopopn.factory import HelloPopnFactory
from bemani.backend.hellopopn.base import HelloPopnBase

__all__ = [
"HelloPopnFactory",
"HelloPopnBase",
]
19 changes: 19 additions & 0 deletions bemani/backend/hellopopn/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing_extensions import Final
from bemani.backend.base import Base
from bemani.backend.core import CoreHandler, CardManagerHandler, PASELIHandler
from bemani.common import GameConstants


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 changes: 27 additions & 0 deletions bemani/backend/hellopopn/factory.py
Original file line number Diff line number Diff line change
@@ -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
259 changes: 259 additions & 0 deletions bemani/backend/hellopopn/hellopopn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
# 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 {
'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)
chara = int(last.attribute('chara'))
for achievement in achievements:
if achievement.type == 'toki_love' and achievement.id == chara:
love = int(achievement.data["love"])
if love < 5:
self.data.local.user.put_achievement(
self.game,
self.version,
userid,
chara,
'toki_love',
{
'love': str(love + 1),
},
)
break

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 changes: 6 additions & 0 deletions bemani/client/hellopopn/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from bemani.client.hellopopn.hellopopn import HelloPopnMuiscClient


__all__ = [
"HelloPopnMuiscClient",
]
Loading