diff --git a/bemani/backend/bishi/bishi.py b/bemani/backend/bishi/bishi.py index 3955927..632412c 100644 --- a/bemani/backend/bishi/bishi.py +++ b/bemani/backend/bishi/bishi.py @@ -2,11 +2,12 @@ import binascii import copy import base64 -from typing import Any, Dict, List +from collections import Iterable +from typing import Any, Dict, List, Sequence, Union from bemani.backend.bishi.base import BishiBashiBase from bemani.backend.ess import EventLogHandler -from bemani.common import ValidatedDict, GameConstants, VersionConstants +from bemani.common import ValidatedDict, GameConstants, VersionConstants, Time from bemani.data import UserID from bemani.protocol import Node @@ -27,11 +28,39 @@ class TheStarBishiBashi( return { 'bools': [ { - 'name': 'Force Unlock Characters', + 'name': 'Force Unlock All Characters', 'tip': 'Force unlock all characters on select screen.', 'category': 'game_config', 'setting': 'force_unlock_characters', }, + { + 'name': 'Unlock Non-Gacha Characters', + 'tip': 'Unlock characters that require playing a different game to unlock.', + 'category': 'game_config', + 'setting': 'force_unlock_eamuse_characters', + }, + { + 'name': 'Enable DLC levels', + 'tip': 'Enable extra DLC levels on newer cabinets.', + 'category': 'game_config', + 'setting': 'enable_dlc_levels', + }, + ], + 'strs': [ + { + 'name': 'Scrolling Announcement', + 'tip': 'An announcement that scrolls by in attract mode.', + 'category': 'game_config', + 'setting': 'big_announcement', + }, + ], + 'longstrs': [ + { + 'name': 'Bulletin Board Announcement', + 'tip': 'An announcement displayed on a bulletin board in attract mode.', + 'category': 'game_config', + 'setting': 'bb_announcement', + }, ], } @@ -53,10 +82,122 @@ class TheStarBishiBashi( return self.update_machine_name(shopname) + def __escape_string(self, data: Union[int, str]) -> str: + data = str(data) + data = data.replace("#", "##") + data = data.replace("\n", "#n") + data = data.replace(" ", "#s") + data = data.replace(",", "#,") + data = data.replace("=", "#=") + data = data.replace(";", "#;") + return data + + def __generate_setting(self, key: str, values: Union[int, str, Sequence[int], Sequence[str]]) -> str: + if isinstance(values, Iterable) and not isinstance(values, str): + values = ",".join(self.__escape_string(x) for x in values) + else: + values = self.__escape_string(values) + key = self.__escape_string(key) + return f"{key}={values}" + 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') - root.add_child(Node.s32('result', 0)) + + if reqtype == "S_SRVMSG" and reqkey == "INFO": + # Settings that we can tweak from the server. + # There's a variety of settings that the game supports, not all of them are figured + # out. They are documented below. + # + # "MAL": 1 - Unlock all DLC levels. + # "MO": [, , ...] - unlock certain DLC levels by ID. The four + # DLC levels are as follows: + # 14 - Morse Code + # 51 - PiroPiro + # 60 - Pop'n Music + # 61 - Love Drop + # "CM": "Arbitrary String" - Scroll the message "Arbitrary String" in attract mode. + # "IM": "Arbitrary Message" - Display "Arbitrary Message" on a new bulletin in attract mode. + # "ALL": 1 - Force-unlock all non-gacha characters. + # "MD": [, , ...] - Unknown setting related to demo mode. Possibly allows server-selection of + # which levels show up? + # "MQ": 0/1 - Unknown boolean setting that enables recommendation weights I think? + # "MR": [, , ...] - Unknown setting related to recommendation weights. Only appears to be used + # if "MQ" is set to 1. + # + # Additionally, there are a series of settings that are related to character unlocks and BGM selection. + # I haven't figured out what this setting does, but it might enable gacha-pulls of characters that otherwise + # require eAmusement plays to unlock? The settings are all in the form of "": . I am not sure what + # the str value should be. They are reproduced here: + # "ABB" = "BishiBashi" + # "ASF" = "Spin Fever" + # "AEK" = "Eternal Knights 2" + # "AOD" = "Otomedius" + # "ABM" = "Beatmania IIDX" + # "APM" = "pop'n music" + # "ATB" = "Twinbee" + # "AGG" = "Good Luck Goemon!" + # "AGK" = "Ga-Ko Kerotan" + # "AQM" = "Quiz Magic Academy" + # "AMF" = "Mahjong Fight Club" + # "AGF" = "Guitar Freaks" + # "ADM" = "DrumMania" + # "AJB" = "Jubeat" + # "ACL" = "Brain Development Institute Kurukuru Lab" + # "ASH" = "Silent Hill THE ARCADE" + # "AHR" = "Horse Riders" + # "AAD" = "Action Detective" + # "AWE" = "Winning Eleven" + # "ACV" = "Ajumajo Dracula (Castlevania)" + # "AGT" = "GTI Club" + # "ABH" = "Baseball Heroes" + # "ADR" = "DanceDanceRevolution" + # "AGD" = "Gradius" + # "APD" = "Parodius" + # "AGC" = "GrandCross Premium" + # "AXX" = "XeXeX" + # "ATK" = "TokiMeki Memorial" + # "AKK" = "Konami" + # "A--" = "Original" + settings: Dict[str, Union[int, str, Sequence[int], Sequence[str]]] = {} + + game_config = self.get_game_config() + enable_dlc_levels = game_config.get_bool('enable_dlc_levels') + if enable_dlc_levels: + settings['MAL'] = 1 + force_unlock_characters = game_config.get_bool('force_unlock_eamuse_characters') + if force_unlock_characters: + settings['ALL'] = 1 + scrolling_message = game_config.get_str('big_announcement') + if scrolling_message: + settings['CM'] = scrolling_message + bb_message = game_config.get_str('bb_announcement') + if bb_message: + settings['IM'] = bb_message + + # Generate system message + settings_str = ";".join(self.__generate_setting(key, vals) for key, vals in settings.items()) + + # Send it to the client, making sure to inform the client that it was valid. + root.add_child(Node.string('strdata1', base64.b64encode(settings_str.encode('ascii')).decode('ascii'))) + root.add_child(Node.string('strdata2', "")) + 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: diff --git a/bemani/frontend/arcade/arcade.py b/bemani/frontend/arcade/arcade.py index 3aeb811..5f22907 100644 --- a/bemani/frontend/arcade/arcade.py +++ b/bemani/frontend/arcade/arcade.py @@ -98,36 +98,35 @@ def get_game_settings(arcade: Arcade) -> List[Dict[str, Any]]: 'name': game_lut[game][version], 'bools': [], 'ints': [], + 'strs': [], + 'longstrs': [], } # Now, look up the current setting for each returned setting - for bool_setting in settings.get('bools', []): - if bool_setting['category'] not in settings_lut[game][version]: - cached_setting = g.data.local.machine.get_settings(arcade.id, game, version, bool_setting['category']) - if cached_setting is None: - cached_setting = ValidatedDict() - settings_lut[game][version][bool_setting['category']] = cached_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][bool_setting['category']] - bool_setting['value'] = current_settings.get_bool(bool_setting['setting']) - game_settings['bools'].append(bool_setting) - - # Now, look up the current setting for each returned setting - for int_setting in settings.get('ints', []): - if int_setting['category'] not in settings_lut[game][version]: - cached_setting = g.data.local.machine.get_settings(arcade.id, game, version, int_setting['category']) - if cached_setting is None: - cached_setting = ValidatedDict() - settings_lut[game][version][int_setting['category']] = cached_setting - - current_settings = settings_lut[game][version][int_setting['category']] - int_setting['value'] = current_settings.get_int(int_setting['setting']) - game_settings['ints'].append(int_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 all_settings + return sorted( + all_settings, + key=lambda setting: (setting['game'], setting['version']), + ) @arcade_pages.route('/') @@ -334,37 +333,27 @@ def updatesettings(arcadeid: int) -> Dict[str, Any]: game = request.get_json()['game'] version = request.get_json()['version'] - for game_setting in request.get_json()['bools']: - # Grab the value to update - category = game_setting['category'] - setting = game_setting['setting'] - new_value = game_setting['value'] + 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(arcade.id, game, version, category) - if current_settings is None: - current_settings = ValidatedDict() + # Update the value + current_settings = g.data.local.machine.get_settings(arcade.id, game, version, category) + if current_settings is None: + current_settings = ValidatedDict() - current_settings.replace_bool(setting, new_value) + getattr(current_settings, update_function)(setting, new_value) - # Save it back - g.data.local.machine.put_settings(arcade.id, game, version, category, current_settings) - - for game_setting in request.get_json()['ints']: - # 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(arcade.id, game, version, category) - if current_settings is None: - current_settings = ValidatedDict() - - current_settings.replace_int(setting, new_value) - - # Save it back - g.data.local.machine.put_settings(arcade.id, game, version, category, current_settings) + # Save it back + g.data.local.machine.put_settings(arcade.id, game, version, category, current_settings) # Return the updated value return { diff --git a/bemani/frontend/static/controllers/arcade/arcade.react.js b/bemani/frontend/static/controllers/arcade/arcade.react.js index f744e5f..44f555c 100644 --- a/bemani/frontend/static/controllers/arcade/arcade.react.js +++ b/bemani/frontend/static/controllers/arcade/arcade.react.js @@ -421,6 +421,53 @@ var arcade_management = React.createClass({ ); }.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 ( +
+ + +