Support server-side settings for BishiBashi, add DLC level enable, non-gacha character enable and announcement/message board support.
This commit is contained in:
parent
0bb19948be
commit
d4ba50a5cd
@ -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:
|
||||
|
@ -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 {
|
||||
|
@ -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]}
|
||||
|
Loading…
x
Reference in New Issue
Block a user