1
0
mirror of synced 2025-01-18 22:24:04 +01:00

Merge pull request #37 from cracrayol/trunk

MGA / Pnm peace / Pnm score card
This commit is contained in:
Jennifer Taylor 2021-09-06 14:47:58 -04:00 committed by GitHub
commit 34884d8377
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1544 additions and 16 deletions

View File

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

View File

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

View File

@ -0,0 +1,8 @@
from bemani.backend.mga.factory import MetalGearArcadeFactory
from bemani.backend.mga.base import MetalGearArcadeBase
__all__ = [
"MetalGearArcadeFactory",
"MetalGearArcadeBase",
]

View 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

345
bemani/client/mga/mga.py Normal file
View 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 = ''
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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
from bemani.frontend.mga.endpoints import mga_pages
from bemani.frontend.mga.cache import MetalGearArcadeCache
__all__ = [
"MetalGearArcadeCache",
"mga_pages",
]

View File

@ -0,0 +1,8 @@
from bemani.data import Config, Data
class MetalGearArcadeCache:
@classmethod
def preload(cls, data: Data, config: Config) -> None:
pass

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

View 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'),
}

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

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

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

View File

@ -2,6 +2,10 @@
border-color: #EF6A32;
}
.mga.border {
border-color: #ff0000;
}
.ddr.border {
border-color: #017351;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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