1
0
mirror of synced 2025-01-18 22:24:04 +01:00

Support server-side settings for BishiBashi, add DLC level enable, non-gacha character enable and announcement/message board support.

This commit is contained in:
Jennifer Taylor 2020-08-26 05:14:13 +00:00
parent 0bb19948be
commit d4ba50a5cd
3 changed files with 231 additions and 54 deletions

View File

@ -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": [<levelnum>, <levelnum>, ...] - 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": [<int>, <int>, ...] - 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": [<int>, <int>, ...] - 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 "<key>": <str>. 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:

View File

@ -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('/<int:arcadeid>')
@ -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 {

View File

@ -421,6 +421,53 @@ var arcade_management = React.createClass({
</div>
);
}.bind(this))}
{ this.state.settings[this.getSettingIndex(this.state.current_setting)].strs.map(function(setting, index) {
return (
<div className="arcade menuoption">
<Tip
text={setting.tip}
>
<label htmlFor={setting.setting}>{setting.name}:</label>
<input
name={setting.setting}
id={setting.setting}
type="text"
value={setting.value}
onChange={function(event) {
this.state.settings[this.getSettingIndex(this.state.current_setting)].strs[index].value = event.target.value;
this.setState({
settings: this.state.settings,
settings_changed: this.setChanged(true),
});
}.bind(this)}
/>
</Tip>
</div>
);
}.bind(this))}
{ this.state.settings[this.getSettingIndex(this.state.current_setting)].longstrs.map(function(setting, index) {
return (
<div className="arcade menuoption">
<Tip
text={setting.tip}
>
<label htmlFor={setting.setting}>{setting.name}:</label>
<textarea
name={setting.setting}
id={setting.setting}
value={setting.value}
onChange={function(event) {
this.state.settings[this.getSettingIndex(this.state.current_setting)].longstrs[index].value = event.target.value;
this.setState({
settings: this.state.settings,
settings_changed: this.setChanged(true),
});
}.bind(this)}
/>
</Tip>
</div>
);
}.bind(this))}
<input
type="submit"
disabled={!this.state.settings_changed[this.state.current_setting]}