Add basic Metal Gear Arcade support
This commit is contained in:
parent
ff63b35de3
commit
8dccd48faf
@ -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
|
||||
|
@ -222,7 +222,7 @@ 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
|
||||
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
|
||||
|
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 Any, Dict, 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
|
@ -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
|
||||
|
||||
|
@ -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
|
175
bemani/frontend/mga/endpoints.py
Normal file
175
bemani/frontend/mga/endpoints.py
Normal file
@ -0,0 +1,175 @@
|
||||
# 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) > 6:
|
||||
raise Exception('Invalid profile name!')
|
||||
|
||||
# Convert lowercase to uppercase. We allow lowercase widetext in
|
||||
# the JS frontend to allow for Windows IME input of hiragana/katakana.
|
||||
def conv(char: str) -> str:
|
||||
i = ord(char)
|
||||
if i >= 0xFF41 and i <= 0xFF5A:
|
||||
return chr(i - (0xFF41 - 0xFF21))
|
||||
else:
|
||||
return char
|
||||
name = ''.join([conv(a) for a in name])
|
||||
|
||||
if re.match(
|
||||
"^[" +
|
||||
"\uFF20-\uFF3A" + # widetext A-Z, @
|
||||
"\uFF10-\uFF19" + # widetext 0-9
|
||||
"\u3041-\u308D\u308F\u3092\u3093" + # hiragana
|
||||
"\u30A1-\u30ED\u30EF\u30F2\u30F3\u30FC" + # katakana
|
||||
"\u3000" + # widetext blank space
|
||||
"\u301C" + # widetext ~
|
||||
"\u30FB" + # widetext middot
|
||||
"\u30FC" + # widetext long dash
|
||||
"\u2212" + # widetext short dash
|
||||
"\u2605" + # widetext heavy star
|
||||
"\uFF01" + # widetext !
|
||||
"\uFF03" + # widetext #
|
||||
"\uFF04" + # widetext $
|
||||
"\uFF05" + # widetext %
|
||||
"\uFF06" + # widetext &
|
||||
"\uFF08" + # widetext (
|
||||
"\uFF09" + # widetext )
|
||||
"\uFF0A" + # widetext *
|
||||
"\uFF0B" + # widetext +
|
||||
"\uFF0F" + # widetext /
|
||||
"\uFF1C" + # widetext <
|
||||
"\uFF1D" + # widetext =
|
||||
"\uFF1E" + # widetext >
|
||||
"\uFF1F" + # widetext ?
|
||||
"\uFFE5" + # widetext Yen symbol
|
||||
"]*$",
|
||||
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[str, 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')
|
||||
);
|
242
bemani/frontend/static/controllers/mga/settings.react.js
Normal file
242
bemani/frontend/static/controllers/mga/settings.react.js
Normal file
@ -0,0 +1,242 @@
|
||||
/*** @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="6"
|
||||
size="6"
|
||||
autofocus="true"
|
||||
ref={c => (this.focus_element = c)}
|
||||
value={this.state.new_name}
|
||||
onChange={function(event) {
|
||||
var rawvalue = event.target.value;
|
||||
var value = "";
|
||||
// Nasty conversion to change typing into wide text
|
||||
for (var i = 0; i < rawvalue.length; i++) {
|
||||
var c = rawvalue.charCodeAt(i);
|
||||
if (c >= '0'.charCodeAt(0) && c <= '9'.charCodeAt(0)) {
|
||||
c = 0xFF10 + (c - '0'.charCodeAt(0));
|
||||
} else if(c >= 'A'.charCodeAt(0) && c <= 'Z'.charCodeAt(0)) {
|
||||
c = 0xFF21 + (c - 'A'.charCodeAt(0));
|
||||
} else if(c >= 'a'.charCodeAt(0) && c <= 'z'.charCodeAt(0)) {
|
||||
c = 0xFF41 + (c - 'a'.charCodeAt(0));
|
||||
} else if(c == '@'.charCodeAt(0)) {
|
||||
c = 0xFF20;
|
||||
} else if(c == ' '.charCodeAt(0)) {
|
||||
c = 0x3000;
|
||||
} else if(c == '~'.charCodeAt(0)) {
|
||||
c = 0x301C;
|
||||
} else if(c == '-'.charCodeAt(0)) {
|
||||
c = 0x2212;
|
||||
} else if(c == '!'.charCodeAt(0)) {
|
||||
c = 0xFF01;
|
||||
} else if(c == '#'.charCodeAt(0)) {
|
||||
c = 0xFF03;
|
||||
} else if(c == '$'.charCodeAt(0)) {
|
||||
c = 0xFF04;
|
||||
} else if(c == '%'.charCodeAt(0)) {
|
||||
c = 0xFF04;
|
||||
} else if(c == '&'.charCodeAt(0)) {
|
||||
c = 0xFF06;
|
||||
} else if(c == '('.charCodeAt(0)) {
|
||||
c = 0xFF08;
|
||||
} else if(c == ')'.charCodeAt(0)) {
|
||||
c = 0xFF09;
|
||||
} else if(c == '*'.charCodeAt(0)) {
|
||||
c = 0xFF0A;
|
||||
} else if(c == '+'.charCodeAt(0)) {
|
||||
c = 0xFF0B;
|
||||
} else if(c == '/'.charCodeAt(0)) {
|
||||
c = 0xFF0F;
|
||||
} else if(c == '<'.charCodeAt(0)) {
|
||||
c = 0xFF1C;
|
||||
} else if(c == '='.charCodeAt(0)) {
|
||||
c = 0xFF1D;
|
||||
} else if(c == '>'.charCodeAt(0)) {
|
||||
c = 0xFF1E;
|
||||
} else if(c == '?'.charCodeAt(0)) {
|
||||
c = 0xFF1F;
|
||||
}
|
||||
value = value + String.fromCharCode(c);
|
||||
}
|
||||
var nameRegex = new RegExp(
|
||||
"^[" +
|
||||
"\uFF20-\uFF3A" + // widetext A-Z, @
|
||||
"\uFF41-\uFF5A" + // widetext a-z (will be uppercased in backend)
|
||||
"\uFF10-\uFF19" + // widetext 0-9
|
||||
"\u3041-\u308D\u308F\u3092\u3093" + // hiragana
|
||||
"\u30A1-\u30ED\u30EF\u30F2\u30F3\u30FC" + // katakana
|
||||
"\u3000" + // widetext blank space
|
||||
"\u301C" + // widetext ~
|
||||
"\u30FB" + // widetext middot
|
||||
"\u30FC" + // widetext long dash
|
||||
"\u2212" + // widetext short dash
|
||||
"\u2605" + // widetext heavy star
|
||||
"\uFF01" + // widetext !
|
||||
"\uFF03" + // widetext #
|
||||
"\uFF04" + // widetext $
|
||||
"\uFF05" + // widetext %
|
||||
"\uFF06" + // widetext &
|
||||
"\uFF08" + // widetext (
|
||||
"\uFF09" + // widetext )
|
||||
"\uFF0A" + // widetext *
|
||||
"\uFF0B" + // widetext +
|
||||
"\uFF0F" + // widetext /
|
||||
"\uFF1C" + // widetext <
|
||||
"\uFF1D" + // widetext =
|
||||
"\uFF1E" + // widetext >
|
||||
"\uFF1F" + // widetext ?
|
||||
"\uFFE5" + // widetext Yen symbol
|
||||
"]*$"
|
||||
);
|
||||
if (value.length <= 6 && nameRegex.test(value)) {
|
||||
this.setState({new_name: value});
|
||||
}
|
||||
}.bind(this)}
|
||||
name="name"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
value="save"
|
||||
/>
|
||||
<input
|
||||
type="button"
|
||||
value="cancel"
|
||||
onClick={function(event) {
|
||||
this.setState({
|
||||
new_name: this.state.player[this.state.version].name,
|
||||
editing_name: false,
|
||||
});
|
||||
}.bind(this)}
|
||||
/>
|
||||
</form>
|
||||
}</LabelledSection>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.player[this.state.version]) {
|
||||
var player = this.state.player[this.state.version];
|
||||
return (
|
||||
<div>
|
||||
<div className="section">
|
||||
{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()
|
||||
|
@ -1,5 +1,4 @@
|
||||
import argparse
|
||||
|
||||
from bemani.common import GameConstants
|
||||
from bemani.frontend import app, config # noqa: F401
|
||||
from bemani.frontend.account import account_pages
|
||||
@ -9,6 +8,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 +33,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:
|
||||
|
@ -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)
|
||||
|
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',
|
||||
|
Loading…
Reference in New Issue
Block a user