diff --git a/MANIFEST.assets b/MANIFEST.assets index 23e63c0..cc498a7 100644 --- a/MANIFEST.assets +++ b/MANIFEST.assets @@ -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 diff --git a/README.md b/README.md index caa2b38..c27f09b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bemani/backend/mga/__init__.py b/bemani/backend/mga/__init__.py new file mode 100644 index 0000000..1290899 --- /dev/null +++ b/bemani/backend/mga/__init__.py @@ -0,0 +1,8 @@ +from bemani.backend.mga.factory import MetalGearArcadeFactory +from bemani.backend.mga.base import MetalGearArcadeBase + + +__all__ = [ + "MetalGearArcadeFactory", + "MetalGearArcadeBase", +] diff --git a/bemani/backend/mga/base.py b/bemani/backend/mga/base.py new file mode 100644 index 0000000..fec3716 --- /dev/null +++ b/bemani/backend/mga/base.py @@ -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 diff --git a/bemani/backend/mga/factory.py b/bemani/backend/mga/factory.py new file mode 100644 index 0000000..ecae9e8 --- /dev/null +++ b/bemani/backend/mga/factory.py @@ -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 diff --git a/bemani/backend/mga/mga.py b/bemani/backend/mga/mga.py new file mode 100644 index 0000000..b004711 --- /dev/null +++ b/bemani/backend/mga/mga.py @@ -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 diff --git a/bemani/common/constants.py b/bemani/common/constants.py index 9fa865a..60fd2f3 100644 --- a/bemani/common/constants.py +++ b/bemani/common/constants.py @@ -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 diff --git a/bemani/frontend/admin/admin.py b/bemani/frontend/admin/admin.py index 3d20a4c..d4597b0 100644 --- a/bemani/frontend/admin/admin.py +++ b/bemani/frontend/admin/admin.py @@ -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', diff --git a/bemani/frontend/app.py b/bemani/frontend/app.py index e58727c..a70ce1d 100644 --- a/bemani/frontend/app.py +++ b/bemani/frontend/app.py @@ -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 = [] diff --git a/bemani/frontend/arcade/arcade.py b/bemani/frontend/arcade/arcade.py index 1dfc60b..77f7d4f 100644 --- a/bemani/frontend/arcade/arcade.py +++ b/bemani/frontend/arcade/arcade.py @@ -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', diff --git a/bemani/frontend/mga/__init__.py b/bemani/frontend/mga/__init__.py new file mode 100644 index 0000000..0303d1d --- /dev/null +++ b/bemani/frontend/mga/__init__.py @@ -0,0 +1,8 @@ +from bemani.frontend.mga.endpoints import mga_pages +from bemani.frontend.mga.cache import MetalGearArcadeCache + + +__all__ = [ + "MetalGearArcadeCache", + "mga_pages", +] diff --git a/bemani/frontend/mga/cache.py b/bemani/frontend/mga/cache.py new file mode 100644 index 0000000..42fb6a2 --- /dev/null +++ b/bemani/frontend/mga/cache.py @@ -0,0 +1,8 @@ +from bemani.data import Config, Data + + +class MetalGearArcadeCache: + + @classmethod + def preload(cls, data: Data, config: Config) -> None: + pass diff --git a/bemani/frontend/mga/endpoints.py b/bemani/frontend/mga/endpoints.py new file mode 100644 index 0000000..c326d92 --- /dev/null +++ b/bemani/frontend/mga/endpoints.py @@ -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/') +@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//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), + } diff --git a/bemani/frontend/mga/mga.py b/bemani/frontend/mga/mga.py new file mode 100644 index 0000000..2bf88bb --- /dev/null +++ b/bemani/frontend/mga/mga.py @@ -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'), + } diff --git a/bemani/frontend/static/controllers/mga/allplayers.react.js b/bemani/frontend/static/controllers/mga/allplayers.react.js new file mode 100644 index 0000000..1e8698c --- /dev/null +++ b/bemani/frontend/static/controllers/mga/allplayers.react.js @@ -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 ( +
+
+ { player.name }; + }.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} + /> + + + ); + }, +}); + +ReactDOM.render( + React.createElement(all_players, null), + document.getElementById('content') +); diff --git a/bemani/frontend/static/controllers/mga/player.react.js b/bemani/frontend/static/controllers/mga/player.react.js new file mode 100644 index 0000000..713ec33 --- /dev/null +++ b/bemani/frontend/static/controllers/mga/player.react.js @@ -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 ( +
+
+

{player.name}'s profile

+ {this.state.profiles.map(function(version) { + return ( +
+
+ {player.extid} + + + + + + + + {player.plays}回 + +
+
+ {player.shop} + {player.shop_area} +
+
+ ); + } else { + return ( +
+
+ {this.state.profiles.map(function(version) { + return ( +
+
+ This player has no profile for {window.versions[this.state.version]}! +
+
+ ); + } + }, +}); + +ReactDOM.render( + React.createElement(profile_view, null), + document.getElementById('content') +); diff --git a/bemani/frontend/static/controllers/mga/settings.react.js b/bemani/frontend/static/controllers/mga/settings.react.js new file mode 100644 index 0000000..d21fc3c --- /dev/null +++ b/bemani/frontend/static/controllers/mga/settings.react.js @@ -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 ( + { + !this.state.editing_name ? + + {player.name} + + : +
+ (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" + /> + + + + }
+ ); + }, + + render: function() { + if (this.state.player[this.state.version]) { + var player = this.state.player[this.state.version]; + return ( +
+
+ {this.state.profiles.map(function(version) { + return ( +
+
+

User Profile

+ {this.renderName(player)} +
+
+ ); + } else { + return ( +
+
+ You have no profile for {window.versions[this.state.version]}! +
+
+ {this.state.profiles.map(function(version) { + return ( +
+
+ ); + } + }, +}); + +ReactDOM.render( + React.createElement(settings_view, null), + document.getElementById('content') +); diff --git a/bemani/frontend/static/default/color.css b/bemani/frontend/static/default/color.css index 9c18e0f..21c279b 100644 --- a/bemani/frontend/static/default/color.css +++ b/bemani/frontend/static/default/color.css @@ -2,6 +2,10 @@ border-color: #EF6A32; } +.mga.border { + border-color: #ff0000; +} + .ddr.border { border-color: #017351; } diff --git a/bemani/utils/config.py b/bemani/utils/config.py index e217a5a..5dc4d4c 100644 --- a/bemani/utils/config.py +++ b/bemani/utils/config.py @@ -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() diff --git a/bemani/utils/frontend.py b/bemani/utils/frontend.py index 8cc0a5f..8984bb5 100644 --- a/bemani/utils/frontend.py +++ b/bemani/utils/frontend.py @@ -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: diff --git a/bemani/utils/scheduler.py b/bemani/utils/scheduler.py index 3ed01a5..8c59434 100644 --- a/bemani/utils/scheduler.py +++ b/bemani/utils/scheduler.py @@ -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) diff --git a/setup.py b/setup.py index fd579f4..2ac31bf 100644 --- a/setup.py +++ b/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',