Merge pull request #37 from cracrayol/trunk
MGA / Pnm peace / Pnm score card
This commit is contained in:
commit
34884d8377
@ -19,6 +19,7 @@ include bemani/frontend/static/controllers/iidx/*.js
|
||||
include bemani/frontend/static/controllers/popn/*.js
|
||||
include bemani/frontend/static/controllers/jubeat/*.js
|
||||
include bemani/frontend/static/controllers/bishi/*.js
|
||||
include bemani/frontend/static/controllers/mga/*.js
|
||||
include bemani/frontend/static/controllers/ddr/*.js
|
||||
include bemani/frontend/static/controllers/reflec/*.js
|
||||
include bemani/frontend/static/controllers/sdvx/*.js
|
||||
|
@ -219,10 +219,10 @@ This should be given the same config file as "api", "frontend" and "services".
|
||||
Development version of an eAmusement protocol server using flask and the protocol
|
||||
libraries also used in "bemanishark" and "trafficgen". Currently it lets most modern
|
||||
BEMANI games boot and supports full profile and events for Beatmania IIDX 20-26,
|
||||
Pop'n Music 19-24, Jubeat Saucer, Saucer Fulfill, Prop, Qubell and Clan, Sound Voltex
|
||||
Pop'n Music 19-25, Jubeat Saucer, Saucer Fulfill, Prop, Qubell and Clan, 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, and finally The\*BishiBashi.
|
||||
1 and Volzza 2, Metal Gear Arcade, and finally The\*BishiBashi.
|
||||
|
||||
Do not use this utility to serve production traffic. Instead, see
|
||||
`bemani/wsgi/api.wsgi` for a ready-to-go WSGI file that can be used with a Python
|
||||
@ -251,7 +251,7 @@ 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-24, Jubeat
|
||||
currently generates traffic emulating Beatmania IIDX 20-26, Pop'n Music 19-25, Jubeat
|
||||
Saucer, Fulfill, Prop, Qubell and Clan, 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
|
||||
@ -398,7 +398,7 @@ do that.
|
||||
### Pop'n Music
|
||||
|
||||
For Pop'n Music, get the game DLL from the version of the game you want to import and
|
||||
run a command like so. This network supports versions 19-24 so you will want to run this
|
||||
run a command like so. This network supports versions 19-25 so you will want to run this
|
||||
command once for every version, giving the correct DLL file:
|
||||
|
||||
```
|
||||
|
8
bemani/backend/mga/__init__.py
Normal file
8
bemani/backend/mga/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
from bemani.backend.mga.factory import MetalGearArcadeFactory
|
||||
from bemani.backend.mga.base import MetalGearArcadeBase
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MetalGearArcadeFactory",
|
||||
"MetalGearArcadeBase",
|
||||
]
|
21
bemani/backend/mga/base.py
Normal file
21
bemani/backend/mga/base.py
Normal file
@ -0,0 +1,21 @@
|
||||
# vim: set fileencoding=utf-8
|
||||
from typing import Optional
|
||||
|
||||
from bemani.backend.base import Base
|
||||
from bemani.backend.core import CoreHandler, CardManagerHandler, PASELIHandler
|
||||
from bemani.common import GameConstants
|
||||
|
||||
|
||||
class MetalGearArcadeBase(CoreHandler, CardManagerHandler, PASELIHandler, Base):
|
||||
"""
|
||||
Base game class for Metal Gear Arcade.
|
||||
"""
|
||||
|
||||
game = GameConstants.MGA
|
||||
|
||||
def previous_version(self) -> Optional['MetalGearArcadeBase']:
|
||||
"""
|
||||
Returns the previous version of the game, based on this game. Should
|
||||
be overridden.
|
||||
"""
|
||||
return None
|
27
bemani/backend/mga/factory.py
Normal file
27
bemani/backend/mga/factory.py
Normal file
@ -0,0 +1,27 @@
|
||||
from typing import Optional
|
||||
|
||||
from bemani.backend.base import Base, Factory
|
||||
from bemani.backend.mga.mga import MetalGearArcade
|
||||
from bemani.common import Model
|
||||
from bemani.data import Config, Data
|
||||
|
||||
|
||||
class MetalGearArcadeFactory(Factory):
|
||||
|
||||
MANAGED_CLASSES = [
|
||||
MetalGearArcade,
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def register_all(cls) -> None:
|
||||
for gamecode in ['I36']:
|
||||
Base.register(gamecode, MetalGearArcadeFactory)
|
||||
|
||||
@classmethod
|
||||
def create(cls, data: Data, config: Config, model: Model, parentmodel: Optional[Model]=None) -> Optional[Base]:
|
||||
|
||||
if model.gamecode == 'I36':
|
||||
return MetalGearArcade(data, config, model)
|
||||
|
||||
# Unknown game version
|
||||
return None
|
177
bemani/backend/mga/mga.py
Normal file
177
bemani/backend/mga/mga.py
Normal file
@ -0,0 +1,177 @@
|
||||
# vim: set fileencoding=utf-8
|
||||
import copy
|
||||
import base64
|
||||
from typing import List
|
||||
|
||||
from bemani.backend.mga.base import MetalGearArcadeBase
|
||||
from bemani.backend.ess import EventLogHandler
|
||||
from bemani.common import Profile, VersionConstants, Time
|
||||
from bemani.data import UserID
|
||||
from bemani.protocol import Node
|
||||
|
||||
|
||||
class MetalGearArcade(
|
||||
EventLogHandler,
|
||||
MetalGearArcadeBase,
|
||||
):
|
||||
|
||||
name = "Metal Gear Arcade"
|
||||
version = VersionConstants.MGA
|
||||
|
||||
def __update_shop_name(self, profiledata: bytes) -> None:
|
||||
# Figure out the profile type
|
||||
csvs = profiledata.split(b',')
|
||||
if len(csvs) < 2:
|
||||
# Not long enough to care about
|
||||
return
|
||||
datatype = csvs[1].decode('ascii')
|
||||
if datatype != 'PLAYDATA':
|
||||
# Not the right profile type requested
|
||||
return
|
||||
|
||||
# Grab the shop name
|
||||
try:
|
||||
shopname = csvs[30].decode('shift-jis')
|
||||
except Exception:
|
||||
return
|
||||
self.update_machine_name(shopname)
|
||||
|
||||
def handle_system_getmaster_request(self, request: Node) -> Node:
|
||||
# See if we can grab the request
|
||||
data = request.child('data')
|
||||
if not data:
|
||||
root = Node.void('system')
|
||||
root.add_child(Node.s32('result', 0))
|
||||
return root
|
||||
|
||||
# Figure out what type of messsage this is
|
||||
reqtype = data.child_value('datatype')
|
||||
reqkey = data.child_value('datakey')
|
||||
|
||||
# System message
|
||||
root = Node.void('system')
|
||||
|
||||
if reqtype == "S_SRVMSG" and reqkey == "INFO":
|
||||
# Generate system message
|
||||
settings1_str = "2011081000:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1"
|
||||
settings2_str = "1,1,1,1,1,1,1,1,1,1,1,1,1,1"
|
||||
|
||||
# Send it to the client, making sure to inform the client that it was valid.
|
||||
root.add_child(Node.string('strdata1', base64.b64encode(settings1_str.encode('ascii')).decode('ascii')))
|
||||
root.add_child(Node.string('strdata2', base64.b64encode(settings2_str.encode('ascii')).decode('ascii')))
|
||||
root.add_child(Node.u64('updatedate', Time.now() * 1000))
|
||||
root.add_child(Node.s32('result', 1))
|
||||
else:
|
||||
# Unknown message.
|
||||
root.add_child(Node.s32('result', 0))
|
||||
|
||||
return root
|
||||
|
||||
def handle_playerdata_usergamedata_send_request(self, request: Node) -> Node:
|
||||
# Look up user by refid
|
||||
refid = request.child_value('data/eaid')
|
||||
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
||||
if userid is None:
|
||||
root = Node.void('playerdata')
|
||||
root.add_child(Node.s32('result', 1)) # Unclear if this is the right thing to do here.
|
||||
return root
|
||||
|
||||
# Extract new profile info from old profile
|
||||
oldprofile = self.get_profile(userid)
|
||||
is_new = False
|
||||
if oldprofile is None:
|
||||
oldprofile = Profile(self.game, self.version, refid, 0)
|
||||
is_new = True
|
||||
newprofile = self.unformat_profile(userid, request, oldprofile, is_new)
|
||||
|
||||
# Write new profile
|
||||
self.put_profile(userid, newprofile)
|
||||
|
||||
# Return success!
|
||||
root = Node.void('playerdata')
|
||||
root.add_child(Node.s32('result', 0))
|
||||
return root
|
||||
|
||||
def handle_playerdata_usergamedata_recv_request(self, request: Node) -> Node:
|
||||
# Look up user by refid
|
||||
refid = request.child_value('data/eaid')
|
||||
profiletypes = request.child_value('data/recv_csv').split(',')
|
||||
profile = None
|
||||
userid = None
|
||||
if refid is not None:
|
||||
userid = self.data.remote.user.from_refid(self.game, self.version, refid)
|
||||
if userid is not None:
|
||||
profile = self.get_profile(userid)
|
||||
if profile is not None:
|
||||
return self.format_profile(userid, profiletypes, profile)
|
||||
else:
|
||||
root = Node.void('playerdata')
|
||||
root.add_child(Node.s32('result', 1)) # Unclear if this is the right thing to do here.
|
||||
return root
|
||||
|
||||
def format_profile(self, userid: UserID, profiletypes: List[str], profile: Profile) -> Node:
|
||||
root = Node.void('playerdata')
|
||||
root.add_child(Node.s32('result', 0))
|
||||
player = Node.void('player')
|
||||
root.add_child(player)
|
||||
records = 0
|
||||
record = Node.void('record')
|
||||
player.add_child(record)
|
||||
|
||||
for profiletype in profiletypes:
|
||||
if profiletype == "3fffffffff":
|
||||
continue
|
||||
for j in range(len(profile['strdatas'])):
|
||||
strdata = profile['strdatas'][j]
|
||||
bindata = profile['bindatas'][j]
|
||||
|
||||
# Figure out the profile type
|
||||
csvs = strdata.split(b',')
|
||||
if len(csvs) < 2:
|
||||
# Not long enough to care about
|
||||
continue
|
||||
datatype = csvs[1].decode('ascii')
|
||||
if datatype != profiletype:
|
||||
# Not the right profile type requested
|
||||
continue
|
||||
|
||||
# This is a valid profile node for this type, lets return only the profile values
|
||||
strdata = b','.join(csvs[2:])
|
||||
d = Node.string('d', base64.b64encode(strdata).decode('ascii'))
|
||||
record.add_child(d)
|
||||
d.add_child(Node.string('bin1', base64.b64encode(bindata).decode('ascii')))
|
||||
|
||||
# Remember that we had this record
|
||||
records = records + 1
|
||||
|
||||
player.add_child(Node.u32('record_num', records))
|
||||
return root
|
||||
|
||||
def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile, is_new: bool) -> Profile:
|
||||
# Profile save request, data values are base64 encoded.
|
||||
# d is a CSV, and bin1 is binary data.
|
||||
newprofile = copy.deepcopy(oldprofile)
|
||||
strdatas: List[bytes] = []
|
||||
bindatas: List[bytes] = []
|
||||
|
||||
record = request.child('data/record')
|
||||
for node in record.children:
|
||||
if node.name != 'd':
|
||||
continue
|
||||
|
||||
profile = base64.b64decode(node.value)
|
||||
# Update the shop name if this is a new profile, since we know it came
|
||||
# from this cabinet. This is the only source of truth for what the
|
||||
# cabinet shop name is set to.
|
||||
if is_new:
|
||||
self.__update_shop_name(profile)
|
||||
strdatas.append(profile)
|
||||
bindatas.append(base64.b64decode(node.child_value('bin1')))
|
||||
|
||||
newprofile['strdatas'] = strdatas
|
||||
newprofile['bindatas'] = bindatas
|
||||
|
||||
# Keep track of play statistics across all versions
|
||||
self.update_play_statistics(userid)
|
||||
|
||||
return newprofile
|
@ -3,7 +3,7 @@ from typing import Dict, Optional, Sequence
|
||||
|
||||
from bemani.backend.base import Base
|
||||
from bemani.backend.core import CoreHandler, CardManagerHandler, PASELIHandler
|
||||
from bemani.common import Profile, ValidatedDict, Time, GameConstants, DBConstants
|
||||
from bemani.common import Profile, ValidatedDict, Time, GameConstants, DBConstants, BroadcastConstants
|
||||
from bemani.data import UserID, Achievement, ScoreSaveException
|
||||
from bemani.protocol import Node
|
||||
|
||||
@ -291,3 +291,47 @@ class PopnMusicBase(CoreHandler, CardManagerHandler, PASELIHandler, Base):
|
||||
|
||||
# We saved successfully
|
||||
break
|
||||
|
||||
def broadcast_score(self, userid: UserID, songid: int, chart: int, medal: int, points: int, combo: int, stats: Dict[str, int]) -> None:
|
||||
# Generate scorecard
|
||||
profile = self.get_profile(userid)
|
||||
song = self.data.local.music.get_song(self.game, self.version, songid, chart)
|
||||
|
||||
card_medal = {
|
||||
self.PLAY_MEDAL_CIRCLE_FAILED: 'Failed',
|
||||
self.PLAY_MEDAL_DIAMOND_FAILED: 'Failed',
|
||||
self.PLAY_MEDAL_STAR_FAILED: 'Failed',
|
||||
self.PLAY_MEDAL_EASY_CLEAR: 'Cleared',
|
||||
self.PLAY_MEDAL_CIRCLE_CLEARED: 'Cleared',
|
||||
self.PLAY_MEDAL_DIAMOND_CLEARED: 'Cleared',
|
||||
self.PLAY_MEDAL_STAR_CLEARED: 'Cleared',
|
||||
self.PLAY_MEDAL_CIRCLE_FULL_COMBO: 'Full Combo',
|
||||
self.PLAY_MEDAL_DIAMOND_FULL_COMBO: 'Full Combo',
|
||||
self.PLAY_MEDAL_STAR_FULL_COMBO: 'Full Combo',
|
||||
self.PLAY_MEDAL_PERFECT: 'Perfect',
|
||||
}[medal]
|
||||
|
||||
card_chart = {
|
||||
self.CHART_TYPE_EASY: 'Easy',
|
||||
self.CHART_TYPE_NORMAL: 'Normal',
|
||||
self.CHART_TYPE_HYPER: 'Hyper',
|
||||
self.CHART_TYPE_EX: 'Ex',
|
||||
}[chart]
|
||||
|
||||
# Construct the dictionary for the broadcast
|
||||
card_data = {
|
||||
BroadcastConstants.PLAYER_NAME: profile.get_str('name', 'なし'),
|
||||
BroadcastConstants.SONG_NAME: song.name,
|
||||
BroadcastConstants.ARTIST_NAME: song.artist,
|
||||
BroadcastConstants.DIFFICULTY: card_chart,
|
||||
BroadcastConstants.SCORE: str(points),
|
||||
BroadcastConstants.MEDAL: card_medal,
|
||||
BroadcastConstants.COOLS: str(stats['cool']),
|
||||
BroadcastConstants.GREATS: str(stats['great']),
|
||||
BroadcastConstants.GOODS: str(stats['good']),
|
||||
BroadcastConstants.BADS: str(stats['bad']),
|
||||
BroadcastConstants.COMBO: str(combo),
|
||||
}
|
||||
|
||||
# Try to broadcast out the score to our webhook(s)
|
||||
self.data.triggers.broadcast_score(card_data, self.game, song)
|
||||
|
@ -439,6 +439,10 @@ class PopnMusicEclale(PopnMusicBase):
|
||||
self.GAME_PLAY_MEDAL_PERFECT: self.PLAY_MEDAL_PERFECT,
|
||||
}[medal]
|
||||
self.update_score(userid, songid, chart, points, medal, combo=combo, stats=stats)
|
||||
|
||||
if request.child_value('is_image_store') == 1:
|
||||
self.broadcast_score(userid, songid, chart, medal, points, combo, stats)
|
||||
|
||||
return root
|
||||
|
||||
def format_conversion(self, userid: UserID, profile: Profile) -> Node:
|
||||
@ -557,6 +561,11 @@ class PopnMusicEclale(PopnMusicBase):
|
||||
account.add_child(Node.s16('total_days', statistics.total_days))
|
||||
account.add_child(Node.s16('interval_day', 0))
|
||||
|
||||
# eAmuse account link
|
||||
eaappli = Node.void('eaappli')
|
||||
root.add_child(eaappli)
|
||||
eaappli.add_child(Node.s8('relation', 1))
|
||||
|
||||
# Set up info node
|
||||
info = Node.void('info')
|
||||
root.add_child(info)
|
||||
|
@ -1,13 +1,74 @@
|
||||
# vim: set fileencoding=utf-8
|
||||
from typing import Dict
|
||||
|
||||
from bemani.backend.popn.base import PopnMusicBase
|
||||
from bemani.backend.popn.usaneko import PopnMusicUsaNeko
|
||||
from bemani.common import VersionConstants
|
||||
|
||||
|
||||
class PopnMusicPeace(PopnMusicBase):
|
||||
class PopnMusicPeace(PopnMusicUsaNeko):
|
||||
|
||||
name = "Pop'n Music peace"
|
||||
version = VersionConstants.POPN_MUSIC_PEACE
|
||||
|
||||
# Biggest ID in the music DB
|
||||
GAME_MAX_MUSIC_ID = 1877
|
||||
|
||||
def previous_version(self) -> PopnMusicBase:
|
||||
return PopnMusicUsaNeko(self.data, self.config, self.model)
|
||||
|
||||
def get_phases(self) -> Dict[int, int]:
|
||||
# Event phases
|
||||
# TODO: Hook event mode settings up to the front end.
|
||||
return {
|
||||
# Default song phase availability (0-23)
|
||||
0: 23,
|
||||
# Unknown event (0-2)
|
||||
1: 2,
|
||||
# Unknown event (0-2)
|
||||
2: 2,
|
||||
# Unknown event (0-4)
|
||||
3: 4,
|
||||
# Unknown event (0-1)
|
||||
4: 1,
|
||||
# Enable Net Taisen, including win/loss display on song select (0-1)
|
||||
5: 1,
|
||||
# Enable NAVI-kun shunkyoku toujou, allows song 1608 to be unlocked (0-1)
|
||||
6: 1,
|
||||
# Unknown event (0-1)
|
||||
7: 1,
|
||||
# Unknown event (0-2)
|
||||
8: 2,
|
||||
# Daily Mission (0-2)
|
||||
9: 2,
|
||||
# NAVI-kun Song phase availability (0-30)
|
||||
10: 30,
|
||||
# Unknown event (0-1)
|
||||
11: 1,
|
||||
# Unknown event (0-2)
|
||||
12: 2,
|
||||
# Enable Pop'n Peace preview song (0-1)
|
||||
13: 1,
|
||||
# Unknown event (0-39)
|
||||
14: 39,
|
||||
# Unknown event (0-2)
|
||||
15: 2,
|
||||
# Unknown event (0-3)
|
||||
16: 3,
|
||||
# Unknown event (0-8)
|
||||
17: 8,
|
||||
# Unknown event (0-1)
|
||||
28: 1,
|
||||
# Unknown event (0-1)
|
||||
19: 1,
|
||||
# Unknown event (0-13)
|
||||
20: 13,
|
||||
# Pop'n event archive song phase availability (0-20)
|
||||
21: 20,
|
||||
# Unknown event (0-2)
|
||||
22: 2,
|
||||
# Unknown event (0-1)
|
||||
23: 1,
|
||||
# Unknown event (0-1)
|
||||
24: 1,
|
||||
}
|
||||
|
@ -126,10 +126,10 @@ class PopnMusicUsaNeko(PopnMusicBase):
|
||||
self.update_machine_name(request.child_value('pcb_setting/name'))
|
||||
return Node.void('pcb24')
|
||||
|
||||
def __construct_common_info(self, root: Node) -> None:
|
||||
def get_phases(self) -> Dict[int, int]:
|
||||
# Event phases
|
||||
# TODO: Hook event mode settings up to the front end.
|
||||
phases = {
|
||||
return {
|
||||
# Default song phase availability (0-11)
|
||||
0: 11,
|
||||
# Unknown event (0-2)
|
||||
@ -160,11 +160,12 @@ class PopnMusicUsaNeko(PopnMusicBase):
|
||||
13: 1,
|
||||
}
|
||||
|
||||
for phaseid in phases:
|
||||
def __construct_common_info(self, root: Node) -> None:
|
||||
for phaseid, phase_value in self.get_phases().items():
|
||||
phase = Node.void('phase')
|
||||
root.add_child(phase)
|
||||
phase.add_child(Node.s16('event_id', phaseid))
|
||||
phase.add_child(Node.s16('phase', phases[phaseid]))
|
||||
phase.add_child(Node.s16('phase', phase_value))
|
||||
|
||||
# Gather course information and course ranking for users.
|
||||
course_infos, achievements, profiles = Parallel.execute([
|
||||
@ -690,6 +691,10 @@ class PopnMusicUsaNeko(PopnMusicBase):
|
||||
self.GAME_PLAY_MEDAL_PERFECT: self.PLAY_MEDAL_PERFECT,
|
||||
}[medal]
|
||||
self.update_score(userid, songid, chart, points, medal, combo=combo, stats=stats)
|
||||
|
||||
if request.child_value('is_image_store') == 1:
|
||||
self.broadcast_score(userid, songid, chart, medal, points, combo, stats)
|
||||
|
||||
return root
|
||||
|
||||
def handle_player24_start_request(self, request: Node) -> Node:
|
||||
@ -928,7 +933,7 @@ class PopnMusicUsaNeko(PopnMusicBase):
|
||||
# eAmuse account link
|
||||
eaappli = Node.void('eaappli')
|
||||
root.add_child(eaappli)
|
||||
eaappli.add_child(Node.s8('relation', -1))
|
||||
eaappli.add_child(Node.s8('relation', 1))
|
||||
|
||||
# Player info
|
||||
info = Node.void('info')
|
||||
|
6
bemani/client/mga/__init__.py
Normal file
6
bemani/client/mga/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from bemani.client.mga.mga import MetalGearArcadeClient
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MetalGearArcadeClient",
|
||||
]
|
345
bemani/client/mga/mga.py
Normal file
345
bemani/client/mga/mga.py
Normal file
@ -0,0 +1,345 @@
|
||||
import base64
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from bemani.client.base import BaseClient
|
||||
from bemani.protocol import Node
|
||||
|
||||
|
||||
class MetalGearArcadeClient(BaseClient):
|
||||
NAME = 'TEST'
|
||||
|
||||
def verify_eventlog_write(self, location: str) -> None:
|
||||
call = self.call_node()
|
||||
|
||||
# Construct node
|
||||
eventlog = Node.void('eventlog')
|
||||
call.add_child(eventlog)
|
||||
eventlog.set_attribute('method', 'write')
|
||||
eventlog.add_child(Node.u32('retrycnt', 0))
|
||||
data = Node.void('data')
|
||||
eventlog.add_child(data)
|
||||
data.add_child(Node.string('eventid', 'S_PWRON'))
|
||||
data.add_child(Node.s32('eventorder', 0))
|
||||
data.add_child(Node.u64('pcbtime', int(time.time() * 1000)))
|
||||
data.add_child(Node.s64('gamesession', -1))
|
||||
data.add_child(Node.string('strdata1', '1.9.1'))
|
||||
data.add_child(Node.string('strdata2', ''))
|
||||
data.add_child(Node.s64('numdata1', 1))
|
||||
data.add_child(Node.s64('numdata2', 0))
|
||||
data.add_child(Node.string('locationid', location))
|
||||
|
||||
# Swap with server
|
||||
resp = self.exchange('', call)
|
||||
|
||||
# Verify that response is correct
|
||||
self.assert_path(resp, "response/eventlog/gamesession")
|
||||
self.assert_path(resp, "response/eventlog/logsendflg")
|
||||
self.assert_path(resp, "response/eventlog/logerrlevel")
|
||||
self.assert_path(resp, "response/eventlog/evtidnosendflg")
|
||||
|
||||
def verify_system_getmaster(self) -> None:
|
||||
call = self.call_node()
|
||||
|
||||
# Construct node
|
||||
system = Node.void('system')
|
||||
call.add_child(system)
|
||||
system.set_attribute('method', 'getmaster')
|
||||
data = Node.void('data')
|
||||
system.add_child(data)
|
||||
data.add_child(Node.string('gamekind', 'I36'))
|
||||
data.add_child(Node.string('datatype', 'S_SRVMSG'))
|
||||
data.add_child(Node.string('datakey', 'INFO'))
|
||||
|
||||
# Swap with server
|
||||
resp = self.exchange('', call)
|
||||
|
||||
# Verify that response is correct
|
||||
self.assert_path(resp, "response/system/result")
|
||||
|
||||
def verify_usergamedata_send(self, ref_id: str, msg_type: str) -> None:
|
||||
call = self.call_node()
|
||||
|
||||
# Set up profile write
|
||||
profiledata = [
|
||||
b'ffffffff',
|
||||
b'PLAYDATA',
|
||||
b'8',
|
||||
b'1',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'ffffffffffffa928',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'PLAYER',
|
||||
b'JP-13',
|
||||
b'ea',
|
||||
b'',
|
||||
b'JP-13',
|
||||
b'',
|
||||
b'',
|
||||
b''
|
||||
]
|
||||
|
||||
outfitdata = [
|
||||
b'ffffffff',
|
||||
b'OUTFIT',
|
||||
b'8',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'202000020400800',
|
||||
b'1000100',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAABwACxEWEUgRWBEuADkRZgBmAAAcAAsRFhFIEVgRLgA5EWYAZgAAHAALERYRSBFYES4AORFmAGYA',
|
||||
b'AAAAAA==',
|
||||
b'',
|
||||
]
|
||||
|
||||
weapondata = [
|
||||
b'ffffffff',
|
||||
b'WEAPON',
|
||||
b'8',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'201000000003',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
]
|
||||
|
||||
mainruledata = [
|
||||
b'ffffffff',
|
||||
b'MAINRULE',
|
||||
b'8',
|
||||
b'6',
|
||||
b'800000',
|
||||
b'1',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'0',
|
||||
b'1.000000',
|
||||
b'0.000000',
|
||||
b'10.000000',
|
||||
b'4.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'0.000000',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
||||
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAA=',
|
||||
b'',
|
||||
b'',
|
||||
b'',
|
||||
b'',
|
||||
]
|
||||
|
||||
if msg_type == 'new':
|
||||
# New profile gets blank name, because we save over it at the end of the round.
|
||||
profiledata[27] = b''
|
||||
elif msg_type == 'existing':
|
||||
# Exiting profile gets our hardcoded name saved.
|
||||
profiledata[27] = self.NAME.encode('shift-jis')
|
||||
|
||||
# Construct node
|
||||
playerdata = Node.void('playerdata')
|
||||
call.add_child(playerdata)
|
||||
playerdata.set_attribute('method', 'usergamedata_send')
|
||||
playerdata.add_child(Node.u32('retrycnt', 0))
|
||||
|
||||
data = Node.void('data')
|
||||
playerdata.add_child(data)
|
||||
data.add_child(Node.string('eaid', ref_id))
|
||||
data.add_child(Node.string('gamekind', 'I36'))
|
||||
data.add_child(Node.u32('datanum', 4))
|
||||
record = Node.void('record')
|
||||
data.add_child(record)
|
||||
d = Node.string('d', base64.b64encode(b','.join(profiledata)).decode('ascii'))
|
||||
record.add_child(d)
|
||||
d.add_child(Node.string('bin1', ''))
|
||||
d = Node.string('d', base64.b64encode(b','.join(outfitdata)).decode('ascii'))
|
||||
record.add_child(d)
|
||||
d.add_child(Node.string('bin1', ''))
|
||||
d = Node.string('d', base64.b64encode(b','.join(weapondata)).decode('ascii'))
|
||||
record.add_child(d)
|
||||
d.add_child(Node.string('bin1', ''))
|
||||
d = Node.string('d', base64.b64encode(b','.join(mainruledata)).decode('ascii'))
|
||||
record.add_child(d)
|
||||
d.add_child(Node.string('bin1', ''))
|
||||
|
||||
# Swap with server
|
||||
resp = self.exchange('', call)
|
||||
self.assert_path(resp, "response/playerdata/result")
|
||||
|
||||
def verify_usergamedata_recv(self, ref_id: str) -> str:
|
||||
call = self.call_node()
|
||||
|
||||
# Construct node
|
||||
playerdata = Node.void('playerdata')
|
||||
call.add_child(playerdata)
|
||||
playerdata.set_attribute('method', 'usergamedata_recv')
|
||||
data = Node.void('data')
|
||||
playerdata.add_child(data)
|
||||
data.add_child(Node.string('eaid', ref_id))
|
||||
data.add_child(Node.string('gamekind', 'I36'))
|
||||
data.add_child(Node.u32('recv_num', 4))
|
||||
data.add_child(Node.string('recv_csv', 'PLAYDATA,3fffffffff,OUTFIT,3fffffffff,WEAPON,3fffffffff,MAINRULE,3fffffffff'))
|
||||
|
||||
# Swap with server
|
||||
resp = self.exchange('', call)
|
||||
self.assert_path(resp, "response/playerdata/result")
|
||||
self.assert_path(resp, "response/playerdata/player/record/d/bin1")
|
||||
self.assert_path(resp, "response/playerdata/player/record_num")
|
||||
|
||||
# Grab binary data, parse out name
|
||||
bindata = resp.child_value('playerdata/player/record/d')
|
||||
profiledata = base64.b64decode(bindata).split(b',')
|
||||
|
||||
# We lob off the first two values in returning profile, so the name is offset by two
|
||||
return profiledata[25].decode('shift-jis')
|
||||
|
||||
def verify(self, cardid: Optional[str]) -> None:
|
||||
# Verify boot sequence is okay
|
||||
self.verify_services_get(
|
||||
expected_services=[
|
||||
'pcbtracker',
|
||||
'pcbevent',
|
||||
'local',
|
||||
'message',
|
||||
'facility',
|
||||
'cardmng',
|
||||
'package',
|
||||
'posevent',
|
||||
'pkglist',
|
||||
'dlstatus',
|
||||
'eacoin',
|
||||
'lobby',
|
||||
'ntp',
|
||||
'keepalive'
|
||||
]
|
||||
)
|
||||
paseli_enabled = self.verify_pcbtracker_alive()
|
||||
self.verify_message_get()
|
||||
self.verify_package_list()
|
||||
location = self.verify_facility_get()
|
||||
self.verify_pcbevent_put()
|
||||
self.verify_eventlog_write(location)
|
||||
self.verify_system_getmaster()
|
||||
|
||||
# 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)
|
||||
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')
|
||||
# MGA doesn't read a new profile, it just writes out CSV for a blank one
|
||||
self.verify_usergamedata_send(ref_id, msg_type='new')
|
||||
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')
|
||||
|
||||
if cardid is None:
|
||||
# Verify profile saving
|
||||
name = self.verify_usergamedata_recv(ref_id)
|
||||
if name != '':
|
||||
raise Exception('New profile has a name associated with it!')
|
||||
|
||||
self.verify_usergamedata_send(ref_id, msg_type='existing')
|
||||
name = self.verify_usergamedata_recv(ref_id)
|
||||
if name != self.NAME:
|
||||
raise Exception('Existing profile has no name associated with it!')
|
||||
else:
|
||||
print("Skipping score checks for existing card")
|
@ -14,6 +14,7 @@ class GameConstants(Enum):
|
||||
DDR: Final[str] = 'ddr'
|
||||
IIDX: Final[str] = 'iidx'
|
||||
JUBEAT: Final[str] = 'jubeat'
|
||||
MGA: Final[str] = 'mga'
|
||||
MUSECA: Final[str] = 'museca'
|
||||
POPN_MUSIC: Final[str] = 'pnm'
|
||||
REFLEC_BEAT: Final[str] = 'reflec'
|
||||
@ -89,6 +90,8 @@ class VersionConstants:
|
||||
JUBEAT_CLAN: Final[int] = 12
|
||||
JUBEAT_FESTO: Final[int] = 13
|
||||
|
||||
MGA: Final[int] = 1
|
||||
|
||||
MUSECA: Final[int] = 1
|
||||
MUSECA_1_PLUS: Final[int] = 2
|
||||
|
||||
@ -312,3 +315,10 @@ class BroadcastConstants(Enum):
|
||||
FASTS: Final[str] = 'Fast'
|
||||
GRADE: Final[str] = 'Grade'
|
||||
RATE: Final[str] = 'Score Rate'
|
||||
|
||||
# Added for Pnm
|
||||
PLAYER_NAME: Final[str] = 'Player Name'
|
||||
SCORE: Final[str] = 'Your Score'
|
||||
COOLS: Final[str] = 'Cools'
|
||||
COMBO: Final[str] = 'Combo'
|
||||
MEDAL: Final[str] = 'Medal'
|
||||
|
@ -33,7 +33,7 @@ class Triggers:
|
||||
self.broadcast_score_discord(data, game, song)
|
||||
|
||||
def broadcast_score_discord(self, data: Dict[BroadcastConstants, str], game: GameConstants, song: Song) -> None:
|
||||
if game == GameConstants.IIDX:
|
||||
if game in {GameConstants.IIDX, GameConstants.POPN_MUSIC}:
|
||||
now = datetime.now()
|
||||
|
||||
webhook = DiscordWebhook(url=self.config.webhooks.discord[game])
|
||||
@ -45,7 +45,7 @@ class Triggers:
|
||||
scoreembed.set_author(name=self.config.name, url=song_url)
|
||||
for item, value in data.items():
|
||||
inline = True
|
||||
if item in {BroadcastConstants.DJ_NAME, BroadcastConstants.SONG_NAME, BroadcastConstants.ARTIST_NAME, BroadcastConstants.PLAY_STATS_HEADER}:
|
||||
if item in {BroadcastConstants.DJ_NAME, BroadcastConstants.PLAYER_NAME, BroadcastConstants.SONG_NAME, BroadcastConstants.ARTIST_NAME, BroadcastConstants.PLAY_STATS_HEADER}:
|
||||
inline = False
|
||||
scoreembed.add_embed_field(name=item.value, value=value, inline=inline)
|
||||
webhook.add_embed(scoreembed)
|
||||
|
@ -246,6 +246,7 @@ def viewmachines() -> Response:
|
||||
GameConstants.DDR.value: 'DDR',
|
||||
GameConstants.IIDX.value: 'IIDX',
|
||||
GameConstants.JUBEAT.value: 'Jubeat',
|
||||
GameConstants.MGA.value: 'Metal Gear Arcade',
|
||||
GameConstants.MUSECA.value: 'MÚSECA',
|
||||
GameConstants.POPN_MUSIC.value: 'Pop\'n Music',
|
||||
GameConstants.REFLEC_BEAT.value: 'Reflec Beat',
|
||||
|
@ -335,6 +335,35 @@ def navigation() -> Dict[str, Any]:
|
||||
},
|
||||
)
|
||||
|
||||
if GameConstants.MGA in g.config.support:
|
||||
# Metal Gear Arcade pages
|
||||
mga_entries = []
|
||||
if len([p for p in profiles if p[0] == GameConstants.MGA]) > 0:
|
||||
mga_entries.extend([
|
||||
{
|
||||
'label': 'Game Options',
|
||||
'uri': url_for('mga_pages.viewsettings'),
|
||||
},
|
||||
{
|
||||
'label': 'Personal Profile',
|
||||
'uri': url_for('mga_pages.viewplayer', userid=g.userID),
|
||||
},
|
||||
])
|
||||
mga_entries.extend([
|
||||
{
|
||||
'label': 'All Players',
|
||||
'uri': url_for('mga_pages.viewplayers'),
|
||||
},
|
||||
])
|
||||
pages.append(
|
||||
{
|
||||
'label': 'Metal Gear Arcade',
|
||||
'entries': mga_entries,
|
||||
'base_uri': app.blueprints['mga_pages'].url_prefix,
|
||||
'gamecode': GameConstants.MGA,
|
||||
},
|
||||
)
|
||||
|
||||
if GameConstants.DDR in g.config.support:
|
||||
# DDR pages
|
||||
ddr_entries = []
|
||||
|
@ -33,6 +33,7 @@ def format_machine(machine: Machine) -> Dict[str, Any]:
|
||||
GameConstants.DDR: 'DDR',
|
||||
GameConstants.IIDX: 'IIDX',
|
||||
GameConstants.JUBEAT: 'Jubeat',
|
||||
GameConstants.MGA: 'Metal Gear Arcade',
|
||||
GameConstants.MUSECA: 'MÚSECA',
|
||||
GameConstants.POPN_MUSIC: 'Pop\'n Music',
|
||||
GameConstants.REFLEC_BEAT: 'Reflec Beat',
|
||||
|
8
bemani/frontend/mga/__init__.py
Normal file
8
bemani/frontend/mga/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
from bemani.frontend.mga.endpoints import mga_pages
|
||||
from bemani.frontend.mga.cache import MetalGearArcadeCache
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MetalGearArcadeCache",
|
||||
"mga_pages",
|
||||
]
|
8
bemani/frontend/mga/cache.py
Normal file
8
bemani/frontend/mga/cache.py
Normal file
@ -0,0 +1,8 @@
|
||||
from bemani.data import Config, Data
|
||||
|
||||
|
||||
class MetalGearArcadeCache:
|
||||
|
||||
@classmethod
|
||||
def preload(cls, data: Data, config: Config) -> None:
|
||||
pass
|
144
bemani/frontend/mga/endpoints.py
Normal file
144
bemani/frontend/mga/endpoints.py
Normal file
@ -0,0 +1,144 @@
|
||||
# vim: set fileencoding=utf-8
|
||||
import re
|
||||
from typing import Any, Dict
|
||||
from flask import Blueprint, request, Response, url_for, abort
|
||||
|
||||
from bemani.common import GameConstants
|
||||
from bemani.data import UserID
|
||||
from bemani.frontend.app import loginrequired, jsonify, render_react
|
||||
from bemani.frontend.mga.mga import MetalGearArcadeFrontend
|
||||
from bemani.frontend.templates import templates_location
|
||||
from bemani.frontend.static import static_location
|
||||
from bemani.frontend.types import g
|
||||
|
||||
|
||||
mga_pages = Blueprint(
|
||||
'mga_pages',
|
||||
__name__,
|
||||
url_prefix='/mga',
|
||||
template_folder=templates_location,
|
||||
static_folder=static_location,
|
||||
)
|
||||
|
||||
|
||||
@mga_pages.route('/players')
|
||||
@loginrequired
|
||||
def viewplayers() -> Response:
|
||||
frontend = MetalGearArcadeFrontend(g.data, g.config, g.cache)
|
||||
return render_react(
|
||||
'All MGA Players',
|
||||
'mga/allplayers.react.js',
|
||||
{
|
||||
'players': frontend.get_all_players()
|
||||
},
|
||||
{
|
||||
'refresh': url_for('mga_pages.listplayers'),
|
||||
'player': url_for('mga_pages.viewplayer', userid=-1),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@mga_pages.route('/players/list')
|
||||
@jsonify
|
||||
@loginrequired
|
||||
def listplayers() -> Dict[str, Any]:
|
||||
frontend = MetalGearArcadeFrontend(g.data, g.config, g.cache)
|
||||
return {
|
||||
'players': frontend.get_all_players(),
|
||||
}
|
||||
|
||||
|
||||
@mga_pages.route('/players/<int:userid>')
|
||||
@loginrequired
|
||||
def viewplayer(userid: UserID) -> Response:
|
||||
frontend = MetalGearArcadeFrontend(g.data, g.config, g.cache)
|
||||
djinfo = frontend.get_all_player_info([userid])[userid]
|
||||
if not djinfo:
|
||||
abort(404)
|
||||
latest_version = sorted(djinfo.keys(), reverse=True)[0]
|
||||
|
||||
return render_react(
|
||||
f'{djinfo[latest_version]["name"]}\'s MGA Profile',
|
||||
'mga/player.react.js',
|
||||
{
|
||||
'playerid': userid,
|
||||
'own_profile': userid == g.userID,
|
||||
'player': djinfo,
|
||||
'versions': {version: name for (game, version, name) in frontend.all_games()},
|
||||
},
|
||||
{
|
||||
'refresh': url_for('mga_pages.listplayer', userid=userid),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@mga_pages.route('/players/<int:userid>/list')
|
||||
@jsonify
|
||||
@loginrequired
|
||||
def listplayer(userid: UserID) -> Dict[str, Any]:
|
||||
frontend = MetalGearArcadeFrontend(g.data, g.config, g.cache)
|
||||
djinfo = frontend.get_all_player_info([userid])[userid]
|
||||
|
||||
return {
|
||||
'player': djinfo,
|
||||
}
|
||||
|
||||
|
||||
@mga_pages.route('/options')
|
||||
@loginrequired
|
||||
def viewsettings() -> Response:
|
||||
frontend = MetalGearArcadeFrontend(g.data, g.config, g.cache)
|
||||
userid = g.userID
|
||||
djinfo = frontend.get_all_player_info([userid])[userid]
|
||||
if not djinfo:
|
||||
abort(404)
|
||||
|
||||
return render_react(
|
||||
'Metal Gear Arcade Game Settings',
|
||||
'mga/settings.react.js',
|
||||
{
|
||||
'player': djinfo,
|
||||
'versions': {version: name for (game, version, name) in frontend.all_games()},
|
||||
},
|
||||
{
|
||||
'updatename': url_for('mga_pages.updatename'),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@mga_pages.route('/options/name/update', methods=['POST'])
|
||||
@jsonify
|
||||
@loginrequired
|
||||
def updatename() -> Dict[str, Any]:
|
||||
frontend = MetalGearArcadeFrontend(g.data, g.config, g.cache)
|
||||
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 dj name
|
||||
profile = g.data.local.user.get_profile(GameConstants.MGA, version, user.id)
|
||||
if profile is None:
|
||||
raise Exception('Unable to find profile to update!')
|
||||
if len(name) == 0 or len(name) > 8:
|
||||
raise Exception('Invalid profile name!')
|
||||
|
||||
if re.match(
|
||||
"^[" +
|
||||
"a-z" +
|
||||
"A-Z" +
|
||||
"0-9" +
|
||||
"@!?/=():*^[\\]#;\\-_{}$.+" +
|
||||
"]*$",
|
||||
name,
|
||||
) is None:
|
||||
raise Exception('Invalid profile name!')
|
||||
profile = frontend.update_name(profile, name)
|
||||
g.data.local.user.put_profile(GameConstants.MGA, version, user.id, profile)
|
||||
|
||||
# Return that we updated
|
||||
return {
|
||||
'version': version,
|
||||
'name': frontend.sanitize_name(name),
|
||||
}
|
88
bemani/frontend/mga/mga.py
Normal file
88
bemani/frontend/mga/mga.py
Normal file
@ -0,0 +1,88 @@
|
||||
# vim: set fileencoding=utf-8
|
||||
import copy
|
||||
from typing import Any, Dict, Iterator, Tuple
|
||||
|
||||
from flask_caching import Cache # type: ignore
|
||||
|
||||
from bemani.backend.mga import MetalGearArcadeFactory
|
||||
from bemani.common import Profile, ValidatedDict, ID, GameConstants
|
||||
from bemani.data import Data
|
||||
from bemani.frontend.base import FrontendBase
|
||||
|
||||
|
||||
class MetalGearArcadeFrontend(FrontendBase):
|
||||
|
||||
game = GameConstants.MGA
|
||||
|
||||
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 MetalGearArcadeFactory.all_games()
|
||||
|
||||
def __update_value(self, oldvalue: str, newvalue: bytes) -> str:
|
||||
try:
|
||||
newstr = newvalue.decode('shift-jis')
|
||||
except Exception:
|
||||
newstr = ''
|
||||
if len(newstr) == 0:
|
||||
return oldvalue
|
||||
else:
|
||||
return newstr
|
||||
|
||||
def sanitize_name(self, name: str) -> str:
|
||||
if len(name) == 0:
|
||||
return 'なし'
|
||||
return name
|
||||
|
||||
def update_name(self, profile: Profile, name: str) -> Profile:
|
||||
newprofile = copy.deepcopy(profile)
|
||||
for i in range(len(newprofile['strdatas'])):
|
||||
strdata = newprofile['strdatas'][i]
|
||||
|
||||
# Figure out the profile type
|
||||
csvs = strdata.split(b',')
|
||||
if len(csvs) < 2:
|
||||
# Not long enough to care about
|
||||
continue
|
||||
datatype = csvs[1].decode('ascii')
|
||||
if datatype != 'PLAYDATA':
|
||||
# Not the right profile type requested
|
||||
continue
|
||||
csvs[27] = name.encode('shift-jis')
|
||||
newprofile['strdatas'][i] = b','.join(csvs)
|
||||
|
||||
return newprofile
|
||||
|
||||
def format_profile(self, profile: Profile, playstats: ValidatedDict) -> Dict[str, Any]:
|
||||
name = 'なし' # Nothing
|
||||
shop = '未設定' # Not set
|
||||
shop_area = '未設定' # Not set
|
||||
|
||||
for i in range(len(profile['strdatas'])):
|
||||
strdata = profile['strdatas'][i]
|
||||
|
||||
# Figure out the profile type
|
||||
csvs = strdata.split(b',')
|
||||
if len(csvs) < 2:
|
||||
# Not long enough to care about
|
||||
continue
|
||||
datatype = csvs[1].decode('ascii')
|
||||
if datatype != 'PLAYDATA':
|
||||
# Not the right profile type requested
|
||||
continue
|
||||
|
||||
name = self.__update_value(name, csvs[27])
|
||||
shop = self.__update_value(shop, csvs[30])
|
||||
shop_area = self.__update_value(shop_area, csvs[31])
|
||||
|
||||
return {
|
||||
'name': name,
|
||||
'extid': ID.format_extid(profile.extid),
|
||||
'shop': shop,
|
||||
'shop_area': shop_area,
|
||||
'first_play_time': playstats.get_int('first_play_timestamp'),
|
||||
'last_play_time': playstats.get_int('last_play_timestamp'),
|
||||
'plays': playstats.get_int('total_plays'),
|
||||
}
|
109
bemani/frontend/static/controllers/mga/allplayers.react.js
Normal file
109
bemani/frontend/static/controllers/mga/allplayers.react.js
Normal file
@ -0,0 +1,109 @@
|
||||
/*** @jsx React.DOM */
|
||||
|
||||
var all_players = React.createClass({
|
||||
|
||||
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: 'Metal Gear Arcade ID',
|
||||
render: function(userid) {
|
||||
var player = this.state.players[userid];
|
||||
return player.extid;
|
||||
}.bind(this),
|
||||
sort: function(aid, bid) {
|
||||
var a = this.state.players[aid];
|
||||
var b = this.state.players[bid];
|
||||
return a.extid.localeCompare(b.extid);
|
||||
}.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,
|
||||
},
|
||||
{
|
||||
name: 'Region',
|
||||
render: function(userid) {
|
||||
var player = this.state.players[userid];
|
||||
return player.shop_area;
|
||||
}.bind(this),
|
||||
sort: function(aid, bid) {
|
||||
var a = this.state.players[aid];
|
||||
var b = this.state.players[bid];
|
||||
return a.shop_area.localeCompare(b.shop_area);
|
||||
}.bind(this),
|
||||
},
|
||||
{
|
||||
name: 'Arcade',
|
||||
render: function(userid) {
|
||||
var player = this.state.players[userid];
|
||||
return player.shop;
|
||||
}.bind(this),
|
||||
sort: function(aid, bid) {
|
||||
var a = this.state.players[aid];
|
||||
var b = this.state.players[bid];
|
||||
return a.shop.localeCompare(b.shop);
|
||||
}.bind(this),
|
||||
},
|
||||
]}
|
||||
rows={Object.keys(this.state.players)}
|
||||
paginate={10}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(all_players, null),
|
||||
document.getElementById('content')
|
||||
);
|
108
bemani/frontend/static/controllers/mga/player.react.js
Normal file
108
bemani/frontend/static/controllers/mga/player.react.js
Normal file
@ -0,0 +1,108 @@
|
||||
/*** @jsx React.DOM */
|
||||
|
||||
var valid_versions = Object.keys(window.versions);
|
||||
var pagenav = new History(valid_versions);
|
||||
|
||||
var profile_view = React.createClass({
|
||||
|
||||
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">
|
||||
<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="Register Time">
|
||||
<Timestamp timestamp={player.first_play_time}/>
|
||||
</LabelledSection>
|
||||
<LabelledSection label="Last Play Time">
|
||||
<Timestamp timestamp={player.last_play_time}/>
|
||||
</LabelledSection>
|
||||
<LabelledSection label="Total Plays">
|
||||
{player.plays}回
|
||||
</LabelledSection>
|
||||
</div>
|
||||
<div className="section">
|
||||
<LabelledSection label="Home Shop">{player.shop}</LabelledSection>
|
||||
<LabelledSection label="Home Shop Area">{player.shop_area}</LabelledSection>
|
||||
</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')
|
||||
);
|
171
bemani/frontend/static/controllers/mga/settings.react.js
Normal file
171
bemani/frontend/static/controllers/mga/settings.react.js
Normal file
@ -0,0 +1,171 @@
|
||||
/*** @jsx React.DOM */
|
||||
|
||||
var valid_versions = Object.keys(window.versions);
|
||||
var pagenav = new History(valid_versions);
|
||||
|
||||
var settings_view = React.createClass({
|
||||
|
||||
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>
|
||||
<span>{player.name}</span>
|
||||
<Edit
|
||||
onClick={function(event) {
|
||||
this.setState({editing_name: true});
|
||||
}.bind(this)}
|
||||
/>
|
||||
</span> :
|
||||
<form className="inline" onSubmit={this.saveName}>
|
||||
<input
|
||||
type="text"
|
||||
className="inline"
|
||||
maxlength="8"
|
||||
size="8"
|
||||
autofocus="true"
|
||||
ref={c => (this.focus_element = c)}
|
||||
value={this.state.new_name}
|
||||
onChange={function(event) {
|
||||
var value = event.target.value;
|
||||
var nameRegex = new RegExp(
|
||||
"^[" +
|
||||
"a-z" +
|
||||
"A-Z" +
|
||||
"0-9" +
|
||||
"@!?/=():*^[\\]#;\\-_{}$.+" +
|
||||
"]*$"
|
||||
);
|
||||
if (value.length <= 8 && 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">
|
||||
{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')
|
||||
);
|
@ -2,6 +2,10 @@
|
||||
border-color: #EF6A32;
|
||||
}
|
||||
|
||||
.mga.border {
|
||||
border-color: #ff0000;
|
||||
}
|
||||
|
||||
.ddr.border {
|
||||
border-color: #017351;
|
||||
}
|
||||
|
@ -9,6 +9,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.mga import MetalGearArcadeFactory
|
||||
from bemani.common import GameConstants
|
||||
from bemani.data import Config, Data
|
||||
|
||||
@ -42,3 +43,5 @@ def register_games(config: Config) -> None:
|
||||
ReflecBeatFactory.register_all()
|
||||
if GameConstants.MUSECA in config.support:
|
||||
MusecaFactory.register_all()
|
||||
if GameConstants.MGA in config.support:
|
||||
MetalGearArcadeFactory.register_all()
|
||||
|
@ -9,6 +9,7 @@ from bemani.frontend.home import home_pages
|
||||
from bemani.frontend.iidx import iidx_pages
|
||||
from bemani.frontend.popn import popn_pages
|
||||
from bemani.frontend.bishi import bishi_pages
|
||||
from bemani.frontend.mga import mga_pages
|
||||
from bemani.frontend.jubeat import jubeat_pages
|
||||
from bemani.frontend.ddr import ddr_pages
|
||||
from bemani.frontend.sdvx import sdvx_pages
|
||||
@ -33,6 +34,8 @@ def register_blueprints() -> None:
|
||||
app.register_blueprint(jubeat_pages)
|
||||
if GameConstants.BISHI_BASHI in config.support:
|
||||
app.register_blueprint(bishi_pages)
|
||||
if GameConstants.MGA in config.support:
|
||||
app.register_blueprint(mga_pages)
|
||||
if GameConstants.DDR in config.support:
|
||||
app.register_blueprint(ddr_pages)
|
||||
if GameConstants.SDVX in config.support:
|
||||
|
@ -371,6 +371,7 @@ class ImportPopn(ImportBase):
|
||||
'22': VersionConstants.POPN_MUSIC_LAPISTORIA,
|
||||
'23': VersionConstants.POPN_MUSIC_ECLALE,
|
||||
'24': VersionConstants.POPN_MUSIC_USANEKO,
|
||||
'25': VersionConstants.POPN_MUSIC_PEACE,
|
||||
}.get(version, -1)
|
||||
|
||||
if actual_version == VersionConstants.POPN_MUSIC_TUNE_STREET:
|
||||
@ -382,7 +383,7 @@ class ImportPopn(ImportBase):
|
||||
# Newer pop'n has charts for easy, normal, hyper, another
|
||||
self.charts = [0, 1, 2, 3]
|
||||
else:
|
||||
raise Exception("Unsupported Pop'n Music version, expected one of the following: 19, 20, 21, 22, 23, 24!")
|
||||
raise Exception("Unsupported Pop'n Music version, expected one of the following: 19, 20, 21, 22, 23, 24, 25!")
|
||||
|
||||
super().__init__(config, GameConstants.POPN_MUSIC, actual_version, no_combine, update)
|
||||
|
||||
@ -957,6 +958,104 @@ class ImportPopn(ImportBase):
|
||||
'I'
|
||||
)
|
||||
|
||||
# Decoding function for chart masks
|
||||
def available_charts(mask: int) -> Tuple[bool, bool, bool, bool, bool, bool]:
|
||||
return (
|
||||
mask & 0x0080000 > 0, # Easy chart bit
|
||||
True, # Always a normal chart
|
||||
mask & 0x1000000 > 0, # Hyper chart bit
|
||||
mask & 0x2000000 > 0, # Ex chart bit
|
||||
True, # Always a battle normal chart
|
||||
mask & 0x4000000 > 0, # Battle hyper chart bit
|
||||
)
|
||||
|
||||
elif self.version == VersionConstants.POPN_MUSIC_PEACE:
|
||||
# Based on M39:J:A:A:2020092800
|
||||
|
||||
# Normal offset for music DB, size
|
||||
offset = 0x2C7C78
|
||||
step = 172
|
||||
length = 1877
|
||||
|
||||
# Offset and step of file DB
|
||||
file_offset = 0x2B8010
|
||||
file_step = 32
|
||||
|
||||
# Standard lookups
|
||||
genre_offset = 0
|
||||
title_offset = 1
|
||||
artist_offset = 2
|
||||
comment_offset = 3
|
||||
english_title_offset = 4
|
||||
english_artist_offset = 5
|
||||
extended_genre_offset = -1
|
||||
charts_offset = 8
|
||||
folder_offset = 9
|
||||
|
||||
# Offsets for normal chart difficulties
|
||||
easy_offset = 12
|
||||
normal_offset = 13
|
||||
hyper_offset = 14
|
||||
ex_offset = 15
|
||||
|
||||
# Offsets for battle chart difficulties
|
||||
battle_normal_offset = 16
|
||||
battle_hyper_offset = 17
|
||||
|
||||
# Offsets into which offset to seek to for file lookups
|
||||
easy_file_offset = 18
|
||||
normal_file_offset = 19
|
||||
hyper_file_offset = 20
|
||||
ex_file_offset = 21
|
||||
battle_normal_file_offset = 22
|
||||
battle_hyper_file_offset = 23
|
||||
|
||||
packedfmt = (
|
||||
'<'
|
||||
'I' # Genre
|
||||
'I' # Title
|
||||
'I' # Artist
|
||||
'I' # Comment
|
||||
'I' # English Title
|
||||
'I' # English Artist
|
||||
'H' # ??
|
||||
'H' # ??
|
||||
'I' # Available charts mask
|
||||
'I' # Folder
|
||||
'I' # Event unlocks?
|
||||
'I' # Event unlocks?
|
||||
'B' # Easy difficulty
|
||||
'B' # Normal difficulty
|
||||
'B' # Hyper difficulty
|
||||
'B' # EX difficulty
|
||||
'B' # Battle normal difficulty
|
||||
'B' # Battle hyper difficulty
|
||||
'xx' # Unknown pointer
|
||||
'H' # Easy chart pointer
|
||||
'H' # Normal chart pointer
|
||||
'H' # Hyper chart pointer
|
||||
'H' # EX chart pointer
|
||||
'H' # Battle normal pointer
|
||||
'H' # Battle hyper pointer
|
||||
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||
)
|
||||
|
||||
# Offsets into file DB for finding file and folder.
|
||||
file_folder_offset = 0
|
||||
file_name_offset = 1
|
||||
|
||||
filefmt = (
|
||||
'<'
|
||||
'I' # Folder
|
||||
'I' # Filename
|
||||
'I'
|
||||
'I'
|
||||
'I'
|
||||
'I'
|
||||
'I'
|
||||
'I'
|
||||
)
|
||||
|
||||
# Decoding function for chart masks
|
||||
def available_charts(mask: int) -> Tuple[bool, bool, bool, bool, bool, bool]:
|
||||
return (
|
||||
|
@ -5,6 +5,7 @@ from bemani.backend.popn import PopnMusicFactory
|
||||
from bemani.backend.jubeat import JubeatFactory
|
||||
from bemani.backend.iidx import IIDXFactory
|
||||
from bemani.backend.bishi import BishiBashiFactory
|
||||
from bemani.backend.mga import MetalGearArcadeFactory
|
||||
from bemani.backend.ddr import DDRFactory
|
||||
from bemani.backend.sdvx import SoundVoltexFactory
|
||||
from bemani.backend.reflec import ReflecBeatFactory
|
||||
@ -13,6 +14,7 @@ from bemani.frontend.popn import PopnMusicCache
|
||||
from bemani.frontend.iidx import IIDXCache
|
||||
from bemani.frontend.jubeat import JubeatCache
|
||||
from bemani.frontend.bishi import BishiBashiCache
|
||||
from bemani.frontend.mga import MetalGearArcadeCache
|
||||
from bemani.frontend.ddr import DDRCache
|
||||
from bemani.frontend.sdvx import SoundVoltexCache
|
||||
from bemani.frontend.reflec import ReflecBeatCache
|
||||
@ -40,6 +42,9 @@ def run_scheduled_work(config: Config) -> None:
|
||||
if GameConstants.BISHI_BASHI in config.support:
|
||||
enabled_factories.append(BishiBashiFactory)
|
||||
enabled_caches.append(BishiBashiCache)
|
||||
if GameConstants.MGA in config.support:
|
||||
enabled_factories.append(MetalGearArcadeFactory)
|
||||
enabled_caches.append(MetalGearArcadeCache)
|
||||
if GameConstants.DDR in config.support:
|
||||
enabled_factories.append(DDRFactory)
|
||||
enabled_caches.append(DDRCache)
|
||||
|
@ -55,6 +55,7 @@ from bemani.client.reflec import (
|
||||
ReflecBeatVolzza2,
|
||||
)
|
||||
from bemani.client.bishi import TheStarBishiBashiClient
|
||||
from bemani.client.mga.mga import MetalGearArcadeClient
|
||||
|
||||
|
||||
def get_client(proto: ClientProtocol, pcbid: str, game: str, config: Dict[str, Any]) -> BaseClient:
|
||||
@ -94,6 +95,12 @@ def get_client(proto: ClientProtocol, pcbid: str, game: str, config: Dict[str, A
|
||||
pcbid,
|
||||
config,
|
||||
)
|
||||
if game == 'pnm-peace':
|
||||
return PopnMusicUsaNekoClient(
|
||||
proto,
|
||||
pcbid,
|
||||
config,
|
||||
)
|
||||
if game == 'jubeat-saucer':
|
||||
return JubeatSaucerClient(
|
||||
proto,
|
||||
@ -280,6 +287,12 @@ def get_client(proto: ClientProtocol, pcbid: str, game: str, config: Dict[str, A
|
||||
pcbid,
|
||||
config,
|
||||
)
|
||||
if game == 'metal-gear-arcade':
|
||||
return MetalGearArcadeClient(
|
||||
proto,
|
||||
pcbid,
|
||||
config,
|
||||
)
|
||||
|
||||
raise Exception(f'Unknown game {game}')
|
||||
|
||||
@ -322,6 +335,12 @@ def mainloop(address: str, port: int, configfile: str, action: str, game: str, c
|
||||
'old_profile_model': "M39:J:B:A",
|
||||
'avs': "2.15.8 r6631",
|
||||
},
|
||||
'pnm-peace': {
|
||||
'name': "Pop'n Music peace",
|
||||
'model': "M39:J:B:A:2020092800",
|
||||
'old_profile_model': "M39:J:B:A",
|
||||
'avs': "2.15.8 r6631",
|
||||
},
|
||||
'jubeat-saucer': {
|
||||
'name': "Jubeat Saucer",
|
||||
'model': "L44:J:A:A:2014012802",
|
||||
@ -477,6 +496,11 @@ def mainloop(address: str, port: int, configfile: str, action: str, game: str, c
|
||||
'model': "MBR:J:A:A:2016100400",
|
||||
'avs': "2.15.8 r6631",
|
||||
},
|
||||
'metal-gear-arcade': {
|
||||
'name': "Metal Gear Arcade",
|
||||
'model': "I36:J:A:A:2011092900",
|
||||
'avs': None,
|
||||
},
|
||||
}
|
||||
if action == 'list':
|
||||
for game in sorted([game for game in games]):
|
||||
@ -534,6 +558,7 @@ def main() -> None:
|
||||
'pnm-22': 'pnm-lapistoria',
|
||||
'pnm-23': 'pnm-eclale',
|
||||
'pnm-24': 'pnm-usaneko',
|
||||
'pnm-25': 'pnm-peace',
|
||||
'iidx-20': 'iidx-tricoro',
|
||||
'iidx-21': 'iidx-spada',
|
||||
'iidx-22': 'iidx-pendual',
|
||||
@ -562,6 +587,7 @@ def main() -> None:
|
||||
'reflec-4': 'reflec-groovin-upper',
|
||||
'reflec-5': 'reflec-volzza',
|
||||
'reflec-6': 'reflec-volzza2',
|
||||
'mga': 'metal-gear-arcade',
|
||||
}.get(game, game)
|
||||
|
||||
mainloop(args.address, args.port, args.config, action, game, args.cardid, args.verbose)
|
||||
|
@ -12,6 +12,7 @@ set -e
|
||||
./read --series pnm --version 22 "$@"
|
||||
./read --series pnm --version 23 "$@"
|
||||
./read --series pnm --version 24 "$@"
|
||||
./read --series pnm --version 25 "$@"
|
||||
|
||||
# Init Jubeat
|
||||
./read --series jubeat --version saucer "$@"
|
||||
|
@ -40,7 +40,8 @@ webhooks:
|
||||
discord:
|
||||
iidx:
|
||||
- "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi"
|
||||
|
||||
pnm:
|
||||
- "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi"
|
||||
paseli:
|
||||
# Whether PASELI is enabled on the network.
|
||||
enabled: True
|
||||
@ -50,7 +51,9 @@ paseli:
|
||||
support:
|
||||
# Bishi Bashi frontend/backend enabled.
|
||||
bishi: True
|
||||
# DDR frontend/backend enabled.
|
||||
# Metal Gear Arcade frontend/backend enabled
|
||||
mga: True
|
||||
# DDR frontend/backend enabled
|
||||
ddr: True
|
||||
# IIDX frontend/backend enabled.
|
||||
iidx: True
|
||||
|
2
setup.py
2
setup.py
@ -222,6 +222,7 @@ setup(
|
||||
'bemani.frontend.popn',
|
||||
'bemani.frontend.jubeat',
|
||||
'bemani.frontend.bishi',
|
||||
'bemani.frontend.mga',
|
||||
'bemani.frontend.ddr',
|
||||
'bemani.frontend.sdvx',
|
||||
'bemani.frontend.reflec',
|
||||
@ -235,6 +236,7 @@ setup(
|
||||
'bemani.backend.jubeat',
|
||||
'bemani.backend.popn',
|
||||
'bemani.backend.bishi',
|
||||
'bemani.backend.mga',
|
||||
'bemani.backend.ddr',
|
||||
'bemani.backend.sdvx',
|
||||
'bemani.backend.reflec',
|
||||
|
@ -40,6 +40,7 @@ declare -a arr=(
|
||||
"reflec-4"
|
||||
"reflec-5"
|
||||
"reflec-6"
|
||||
"mga"
|
||||
)
|
||||
|
||||
for project in "${arr[@]}"
|
||||
|
Loading…
x
Reference in New Issue
Block a user