diff --git a/bemani/backend/base.py b/bemani/backend/base.py index 5307430..ca9c001 100644 --- a/bemani/backend/base.py +++ b/bemani/backend/base.py @@ -447,14 +447,22 @@ class Base(ABC): def get_game_config(self) -> ValidatedDict: machine = self.data.local.machine.get_machine(self.config.machine.pcbid) + + # If this machine belongs to an arcade, use its settings. If the settings aren't present, + # default to the game's defaults. if machine.arcade is not None: settings = self.data.local.machine.get_settings(machine.arcade, self.game, self.version, 'game_config') - else: - settings = None + if settings is None: + settings = ValidatedDict() + return settings - if settings is None: - settings = ValidatedDict() - return settings + # If this machine does not belong to an arcade, use the server-wide settings. If the settings + # aren't present, default ot the game's default. + else: + settings = self.data.local.machine.get_settings(self.data.local.machine.DEFAULT_SETTINGS_ARCADE, self.game, self.version, 'game_config') + if settings is None: + settings = ValidatedDict() + return settings def get_play_statistics(self, userid: UserID) -> PlayStatistics: """ diff --git a/bemani/data/mysql/machine.py b/bemani/data/mysql/machine.py index b8e5931..af7888e 100644 --- a/bemani/data/mysql/machine.py +++ b/bemani/data/mysql/machine.py @@ -2,6 +2,7 @@ from sqlalchemy import Table, Column, UniqueConstraint # type: ignore from sqlalchemy.types import String, Integer, JSON # type: ignore from sqlalchemy.dialects.mysql import BIGINT as BigInteger # type: ignore from typing import Optional, Dict, List, Tuple, Any +from typing_extensions import Final from bemani.common import GameConstants, ValidatedDict from bemani.data.mysql.base import BaseData, metadata @@ -80,6 +81,10 @@ class ArcadeCreationException(Exception): class MachineData(BaseData): + # This relies on the fact that arcadeid in the arcade_settings table is auto-increment + # and thus will start at 1. + DEFAULT_SETTINGS_ARCADE: Final[ArcadeID] = ArcadeID(-1) + def from_port(self, port: int) -> Optional[str]: """ Given a port, look up the PCBID attached to that port. diff --git a/bemani/frontend/admin/admin.py b/bemani/frontend/admin/admin.py index 1fa2316..cccd1ac 100644 --- a/bemani/frontend/admin/admin.py +++ b/bemani/frontend/admin/admin.py @@ -3,10 +3,11 @@ from typing import Dict, Tuple, Any, Optional from flask import Blueprint, request, Response, render_template, url_for from bemani.backend.base import Base -from bemani.common import CardCipher, CardCipherException, GameConstants, RegionConstants +from bemani.common import CardCipher, CardCipherException, GameConstants, RegionConstants, ValidatedDict from bemani.data import Arcade, Machine, User, UserID, News, Event, Server, Client from bemani.data.api.client import APIClient, NotAuthorizedAPIException, APIException from bemani.frontend.app import adminrequired, jsonify, valid_email, valid_username, valid_pin, render_react +from bemani.frontend.gamesettings import get_game_settings from bemani.frontend.iidx.iidx import IIDXFrontend from bemani.frontend.jubeat.jubeat import JubeatFrontend from bemani.frontend.popn.popn import PopnMusicFrontend @@ -320,6 +321,21 @@ def viewnews() -> Response: ) +@admin_pages.route('/gamesettings') +@adminrequired +def viewgamesettings() -> Response: + return render_react( + 'Game Settings', + 'admin/gamesettings.react.js', + { + 'game_settings': get_game_settings(g.data, g.data.local.machine.DEFAULT_SETTINGS_ARCADE), + }, + { + 'update_settings': url_for('admin_pages.updatesettings'), + }, + ) + + @admin_pages.route('/users/') @adminrequired def viewuser(userid: int) -> Response: @@ -1081,3 +1097,44 @@ def updatenews() -> Dict[str, Any]: return { 'news': [format_news(news) for news in g.data.local.network.get_all_news()], } + + +@admin_pages.route('/gamesettings/update', methods=['POST']) +@jsonify +@adminrequired +def updatesettings() -> Dict[str, Any]: + # Cast the ID for type safety. + arcadeid = g.data.local.machine.DEFAULT_SETTINGS_ARCADE + + game = GameConstants(request.get_json()['game']) + version = request.get_json()['version'] + + for setting_type, update_function in [ + ('bools', 'replace_bool'), + ('ints', 'replace_int'), + ('strs', 'replace_str'), + ('longstrs', 'replace_str'), + ]: + for game_setting in request.get_json()[setting_type]: + # Grab the value to update + category = game_setting['category'] + setting = game_setting['setting'] + new_value = game_setting['value'] + + # Update the value + current_settings = g.data.local.machine.get_settings(arcadeid, game, version, category) + if current_settings is None: + current_settings = ValidatedDict() + + getattr(current_settings, update_function)(setting, new_value) + + # Save it back + g.data.local.machine.put_settings(arcadeid, game, version, category, current_settings) + + # Return the updated value + return { + 'game_settings': [ + gs for gs in get_game_settings(g.data, arcadeid) + if gs['game'] == game.value and gs['version'] == version + ][0], + } diff --git a/bemani/frontend/app.py b/bemani/frontend/app.py index e8c079e..21a406a 100644 --- a/bemani/frontend/app.py +++ b/bemani/frontend/app.py @@ -726,6 +726,10 @@ def navigation() -> Dict[str, Any]: 'label': 'PCBIDs', 'uri': url_for('admin_pages.viewmachines'), }, + { + 'label': 'Game Settings', + 'uri': url_for('admin_pages.viewgamesettings'), + }, { 'label': 'Cards', 'uri': url_for('admin_pages.viewcards'), diff --git a/bemani/frontend/arcade/arcade.py b/bemani/frontend/arcade/arcade.py index df111a7..7788950 100644 --- a/bemani/frontend/arcade/arcade.py +++ b/bemani/frontend/arcade/arcade.py @@ -6,6 +6,7 @@ from bemani.backend.base import Base from bemani.common import CardCipher, CardCipherException, ValidatedDict, GameConstants, RegionConstants from bemani.data import Arcade, ArcadeID, Event, Machine from bemani.frontend.app import loginrequired, jsonify, render_react, valid_pin +from bemani.frontend.gamesettings import get_game_settings from bemani.frontend.templates import templates_location from bemani.frontend.static import static_location from bemani.frontend.types import g @@ -85,60 +86,6 @@ def format_event(event: Event) -> Dict[str, Any]: } -def get_game_settings(arcade: Arcade) -> List[Dict[str, Any]]: - game_lut: Dict[GameConstants, Dict[int, str]] = {} - settings_lut: Dict[GameConstants, Dict[int, Dict[str, Any]]] = {} - all_settings = [] - - for (game, version, name) in Base.all_games(): - if game not in game_lut: - game_lut[game] = {} - settings_lut[game] = {} - game_lut[game][version] = name - settings_lut[game][version] = {} - - for (game, version, settings) in Base.all_settings(): - if not settings: - continue - - # First, set up the basics - game_settings: Dict[str, Any] = { - 'game': game.value, - 'version': version, - 'name': game_lut[game][version], - 'bools': [], - 'ints': [], - 'strs': [], - 'longstrs': [], - } - - # Now, look up the current setting for each returned setting - for setting_type, setting_unpacker in [ - ('bools', "get_bool"), - ('ints', "get_int"), - ('strs', "get_str"), - ('longstrs', "get_str"), - ]: - for setting in settings.get(setting_type, []): - if setting['category'] not in settings_lut[game][version]: - cached_setting = g.data.local.machine.get_settings(arcade.id, game, version, setting['category']) - if cached_setting is None: - cached_setting = ValidatedDict() - settings_lut[game][version][setting['category']] = cached_setting - - current_settings = settings_lut[game][version][setting['category']] - setting['value'] = getattr(current_settings, setting_unpacker)(setting['setting']) - game_settings[setting_type].append(setting) - - # Now, include it! - all_settings.append(game_settings) - - return sorted( - all_settings, - key=lambda setting: (setting['game'], setting['version']), - ) - - @arcade_pages.route('/') @loginrequired def viewarcade(arcadeid: int) -> Response: @@ -158,7 +105,7 @@ def viewarcade(arcadeid: int) -> Response: 'arcade': format_arcade(arcade), 'regions': RegionConstants.LUT, 'machines': machines, - 'game_settings': get_game_settings(arcade), + 'game_settings': get_game_settings(g.data, arcadeid), 'balances': {balance[0]: balance[1] for balance in g.data.local.machine.get_balances(arcadeid)}, 'users': {user.id: user.username for user in g.data.local.user.get_all_users()}, 'events': [format_event(event) for event in g.data.local.network.get_events(arcadeid=arcadeid, event='paseli_transaction')], @@ -526,7 +473,7 @@ def updatesettings(arcadeid: int) -> Dict[str, Any]: new_value = game_setting['value'] # Update the value - current_settings = g.data.local.machine.get_settings(arcade.id, game, version, category) + current_settings = g.data.local.machine.get_settings(arcadeid, game, version, category) if current_settings is None: current_settings = ValidatedDict() @@ -538,7 +485,7 @@ def updatesettings(arcadeid: int) -> Dict[str, Any]: # Return the updated value return { 'game_settings': [ - gs for gs in get_game_settings(arcade) + gs for gs in get_game_settings(g.data, arcadeid) if gs['game'] == game.value and gs['version'] == version ][0], } diff --git a/bemani/frontend/gamesettings.py b/bemani/frontend/gamesettings.py new file mode 100644 index 0000000..a15475c --- /dev/null +++ b/bemani/frontend/gamesettings.py @@ -0,0 +1,59 @@ +from typing import Any, Dict, List + +from bemani.backend.base import Base +from bemani.common import ValidatedDict, GameConstants +from bemani.data import Data, ArcadeID + + +def get_game_settings(data: Data, arcadeid: ArcadeID) -> List[Dict[str, Any]]: + game_lut: Dict[GameConstants, Dict[int, str]] = {} + settings_lut: Dict[GameConstants, Dict[int, Dict[str, Any]]] = {} + all_settings = [] + + for (game, version, name) in Base.all_games(): + if game not in game_lut: + game_lut[game] = {} + settings_lut[game] = {} + game_lut[game][version] = name + settings_lut[game][version] = {} + + for (game, version, settings) in Base.all_settings(): + if not settings: + continue + + # First, set up the basics + game_settings: Dict[str, Any] = { + 'game': game.value, + 'version': version, + 'name': game_lut[game][version], + 'bools': [], + 'ints': [], + 'strs': [], + 'longstrs': [], + } + + # Now, look up the current setting for each returned setting + for setting_type, setting_unpacker in [ + ('bools', "get_bool"), + ('ints', "get_int"), + ('strs', "get_str"), + ('longstrs', "get_str"), + ]: + for setting in settings.get(setting_type, []): + if setting['category'] not in settings_lut[game][version]: + cached_setting = data.local.machine.get_settings(arcadeid, game, version, setting['category']) + if cached_setting is None: + cached_setting = ValidatedDict() + settings_lut[game][version][setting['category']] = cached_setting + + current_settings = settings_lut[game][version][setting['category']] + setting['value'] = getattr(current_settings, setting_unpacker)(setting['setting']) + game_settings[setting_type].append(setting) + + # Now, include it! + all_settings.append(game_settings) + + return sorted( + all_settings, + key=lambda setting: (setting['game'], setting['version']), + ) diff --git a/bemani/frontend/static/components/gamesettings.react.js b/bemani/frontend/static/components/gamesettings.react.js new file mode 100644 index 0000000..6900ab7 --- /dev/null +++ b/bemani/frontend/static/components/gamesettings.react.js @@ -0,0 +1,210 @@ +/*** @jsx React.DOM */ + +function makeGameSettingName(game_settings) { + return game_settings.game + '-' + game_settings.version; +} + +var GameSettings = React.createClass({ + getInitialState: function() { + console.log(this.props); + var valid_settings = this.props.game_settings.map(function(setting) { + return makeGameSettingName(setting); + }); + var pagenav = new History(valid_settings); + + return { + pagenav: pagenav, + settings: this.props.game_settings, + current_setting: pagenav.getInitialState(makeGameSettingName(this.props.game_settings[0])), + settings_changed: {}, + settings_saving: {}, + settings_saved: {}, + }; + }, + + componentDidMount: function() { + this.state.pagenav.onChange(function(setting) { + this.setState({current_setting: setting}); + }.bind(this)); + }, + + getSettingIndex: function(setting_name) { + var real_index = -1; + this.state.settings.map(function(game_settings, index) { + var current = makeGameSettingName(game_settings); + if (current == setting_name) { real_index = index; } + }.bind(this)); + return real_index; + }, + + setChanged: function(val) { + this.state.settings_changed[this.state.current_setting] = val; + return this.state.settings_changed; + }, + + setSaving: function(val) { + this.state.settings_saving[this.state.current_setting] = val; + return this.state.settings_saving; + }, + + setSaved: function(val) { + this.state.settings_saved[this.state.current_setting] = val; + return this.state.settings_saved; + }, + + saveSettings: function(event) { + var index = this.getSettingIndex(this.state.current_setting); + this.setState({settings_saving: this.setSaving(true), settings_saved: this.setSaved(false)}); + AJAX.post( + Link.get('update_settings'), + this.state.settings[index], + function(response) { + this.state.settings[index] = response.game_settings; + this.setState({ + settings: this.state.settings, + settings_saving: this.setSaving(false), + settings_saved: this.setSaved(true), + settings_changed: this.setChanged(false), + }); + }.bind(this) + ); + event.preventDefault(); + }, + + render: function() { + return ( +
+
+ { this.state.settings.map(function(game_settings) { + var current = makeGameSettingName(game_settings); + return ( +
+
+ { this.state.settings[this.getSettingIndex(this.state.current_setting)].ints.map(function(setting, index) { + return ( +
+ + + + +
+ ); + }.bind(this))} + { this.state.settings[this.getSettingIndex(this.state.current_setting)].bools.map(function(setting, index) { + return ( +
+ + + + +
+ ); + }.bind(this))} + { this.state.settings[this.getSettingIndex(this.state.current_setting)].strs.map(function(setting, index) { + return ( +
+ + + + +
+ ); + }.bind(this))} + { this.state.settings[this.getSettingIndex(this.state.current_setting)].longstrs.map(function(setting, index) { + return ( +
+ + +