1
0
mirror of synced 2024-11-27 15:40:48 +01:00

Add basic Metal Gear Arcade support

This commit is contained in:
cracrayol 2021-09-04 17:17:22 +02:00
parent ff63b35de3
commit 8dccd48faf
22 changed files with 1024 additions and 2 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

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

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

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

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

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

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

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

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

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

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