1
0
mirror of synced 2024-09-24 03:18:22 +02:00

Add config, database and user interface to change network region as well as per-arcade region. Also unified IIDX prefecture setting.

This commit is contained in:
Jennifer Taylor 2021-09-07 02:48:33 +00:00
parent beb818f42b
commit d05c3f907d
22 changed files with 431 additions and 150 deletions

View File

@ -1,7 +1,7 @@
from bemani.common.model import Model from bemani.common.model import Model
from bemani.common.validateddict import ValidatedDict, Profile, PlayStatistics, intish from bemani.common.validateddict import ValidatedDict, Profile, PlayStatistics, intish
from bemani.common.http import HTTP from bemani.common.http import HTTP
from bemani.common.constants import APIConstants, GameConstants, VersionConstants, DBConstants, BroadcastConstants from bemani.common.constants import APIConstants, GameConstants, VersionConstants, DBConstants, BroadcastConstants, RegionConstants
from bemani.common.card import CardCipher, CardCipherException from bemani.common.card import CardCipher, CardCipherException
from bemani.common.id import ID from bemani.common.id import ID
from bemani.common.aes import AESCipher from bemani.common.aes import AESCipher
@ -21,6 +21,7 @@ __all__ = [
"VersionConstants", "VersionConstants",
"DBConstants", "DBConstants",
"BroadcastConstants", "BroadcastConstants",
"RegionConstants",
"CardCipher", "CardCipher",
"CardCipherException", "CardCipherException",
"ID", "ID",

View File

@ -1,4 +1,5 @@
from enum import Enum from enum import Enum
from typing import Dict
from typing_extensions import Final from typing_extensions import Final
@ -322,3 +323,206 @@ class BroadcastConstants(Enum):
COOLS: Final[str] = 'Cools' COOLS: Final[str] = 'Cools'
COMBO: Final[str] = 'Combo' COMBO: Final[str] = 'Combo'
MEDAL: Final[str] = 'Medal' MEDAL: Final[str] = 'Medal'
class _RegionConstants:
"""
Class representing the various region IDs found in all games.
"""
# The following are the original enumerations, that still are correct
# for new games today.
HOKKAIDO: Final[int] = 1
AOMORI: Final[int] = 2
IWATE: Final[int] = 3
MIYAGI: Final[int] = 4
AKITA: Final[int] = 5
YAMAGATA: Final[int] = 6
FUKUSHIMA: Final[int] = 7
IBARAKI: Final[int] = 8
TOCHIGI: Final[int] = 9
GUNMA: Final[int] = 10
SAITAMA: Final[int] = 11
CHIBA: Final[int] = 12
TOKYO: Final[int] = 13
KANAGAWA: Final[int] = 14
NIIGATA: Final[int] = 15
TOYAMA: Final[int] = 16
ISHIKAWA: Final[int] = 17
FUKUI: Final[int] = 18
YAMANASHI: Final[int] = 19
NAGANO: Final[int] = 20
GIFU: Final[int] = 21
SHIZUOKA: Final[int] = 22
AICHI: Final[int] = 23
MIE: Final[int] = 24
SHIGA: Final[int] = 25
KYOTO: Final[int] = 26
OSAKA: Final[int] = 27
HYOGO: Final[int] = 28
NARA: Final[int] = 29
WAKAYAMA: Final[int] = 30
TOTTORI: Final[int] = 31
SHIMANE: Final[int] = 32
OKAYAMA: Final[int] = 33
HIROSHIMA: Final[int] = 34
YAMAGUCHI: Final[int] = 35
TOKUSHIMA: Final[int] = 36
KAGAWA: Final[int] = 37
EHIME: Final[int] = 38
KOUCHI: Final[int] = 39
FUKUOKA: Final[int] = 40
SAGA: Final[int] = 41
NAGASAKI: Final[int] = 42
KUMAMOTO: Final[int] = 43
OITA: Final[int] = 44
MIYAZAKI: Final[int] = 45
KAGOSHIMA: Final[int] = 46
OKINAWA: Final[int] = 47
HONG_KONG: Final[int] = 48
KOREA: Final[int] = 49
TAIWAN: Final[int] = 50
# The following are new additions, replacing the "OLD" values below.
THAILAND: Final[int] = 51
INDONESIA: Final[int] = 52
SINGAPORE: Final[int] = 53
PHILLIPINES: Final[int] = 54
MACAO: Final[int] = 55
USA: Final[int] = 56
OTHER: Final[int] = 57
# Bogus value for europe.
EUROPE: Final[int] = 1000
NO_MAPPING: Final[int] = 2000
# Old constant values.
OLD_USA: Final[int] = 51
OLD_EUROPE: Final[int] = 52
OLD_OTHER: Final[int] = 53
# Min/max valid values for server.
MIN: Final[int] = 1
MAX: Final[int] = 56
# This is a really nasty LUT to attempt to make the frontend display
# the same regardless of the game in question. This is mostly because
# the prefecture/region stored in the profile is editable by IIDX and
# I didn't anticipate this ever changing.
def db_to_game_region(self, use_new_table: bool, region: int) -> int:
if use_new_table:
# The new lookup table does not have Europe as an option.
if region in {RegionConstants.EUROPE, RegionConstants.NO_MAPPING}:
return RegionConstants.OTHER
# The rest matches what we have already.
return region
else:
# The old lookup table supports most of the values.
if region <= RegionConstants.TAIWAN:
return region
# Map the two values that still exist back to their old values.
if region == RegionConstants.USA:
return RegionConstants.OLD_USA
if region == RegionConstants.EUROPE:
return RegionConstants.OLD_EUROPE
# The rest get mapped to other.
return RegionConstants.OLD_OTHER
# This performs the equivalent inverse of the above function. Note that
# depending on the game and selection, this is lossy (as in, Europe could
# get converted to Other, etc).
def game_to_db_region(self, use_new_table: bool, region: int) -> int:
if use_new_table:
if region == RegionConstants.OTHER:
return RegionConstants.NO_MAPPING
# The new lookup table is correct aside from the above correction.
return region
else:
# The old lookup table supports most of the values.
if region <= RegionConstants.TAIWAN:
return region
# Map the three values that might be seen to new DB values.
if region == RegionConstants.OLD_USA:
return RegionConstants.USA
if region == RegionConstants.OLD_EUROPE:
return RegionConstants.EUROPE
if region == RegionConstants.OLD_OTHER:
return RegionConstants.NO_MAPPING
raise Exception(f"Unexpected value {region} for game region!")
@property
def LUT(cls) -> Dict[int, str]:
return {
cls.HOKKAIDO: '北海道 (Hokkaido)',
cls.AOMORI: '青森県 (Aomori)',
cls.IWATE: '岩手県 (Iwate)',
cls.MIYAGI: '宮城県 (Miyagi)',
cls.AKITA: '秋田県 (Akita)',
cls.YAMAGATA: '山形県 (Yamagata)',
cls.FUKUSHIMA: '福島県 (Fukushima)',
cls.IBARAKI: '茨城県 (Ibaraki)',
cls.TOCHIGI: '栃木県 (Tochigi)',
cls.GUNMA: '群馬県 (Gunma)',
cls.SAITAMA: '埼玉県 (Saitama)',
cls.CHIBA: '千葉県 (Chiba)',
cls.TOKYO: '東京都 (Tokyo)',
cls.KANAGAWA: '神奈川県 (Kanagawa)',
cls.NIIGATA: '新潟県 (Niigata)',
cls.TOYAMA: '富山県 (Toyama)',
cls.ISHIKAWA: '石川県 (Ishikawa)',
cls.FUKUI: '福井県 (Fukui)',
cls.YAMANASHI: '山梨県 (Yamanashi)',
cls.NAGANO: '長野県 (Nagano)',
cls.GIFU: '岐阜県 (Gifu)',
cls.SHIZUOKA: '静岡県 (Shizuoka)',
cls.AICHI: '愛知県 (Aichi)',
cls.MIE: '三重県 (Mie)',
cls.SHIGA: '滋賀県 (Shiga)',
cls.KYOTO: '京都府 (Kyoto)',
cls.OSAKA: '大阪府 (Osaka)',
cls.HYOGO: '兵庫県 (Hyogo)',
cls.NARA: '奈良県 (Nara)',
cls.WAKAYAMA: '和歌山県 (Wakayama)',
cls.TOTTORI: '鳥取県 (Tottori)',
cls.SHIMANE: '島根県 (Shimane)',
cls.OKAYAMA: '岡山県 (Okayama)',
cls.HIROSHIMA: '広島県 (Hiroshima)',
cls.YAMAGUCHI: '山口県 (Yamaguchi)',
cls.TOKUSHIMA: '徳島県 (Tokushima)',
cls.KAGAWA: '香川県 (Kagawa)',
cls.EHIME: '愛媛県 (Ehime)',
cls.KOUCHI: '高知県 (Kochi)',
cls.FUKUOKA: '福岡県 (Fukuoka)',
cls.SAGA: '佐賀県 (Saga)',
cls.NAGASAKI: '長崎県 (Nagasaki)',
cls.KUMAMOTO: '熊本県 (Kumamoto)',
cls.OITA: '大分県 (Oita)',
cls.MIYAZAKI: '宮崎県 (Miyazaki)',
cls.KAGOSHIMA: '鹿児島県 (Kagoshima)',
cls.OKINAWA: '沖縄県 (Okinawa)',
cls.HONG_KONG: '香港 (Hong Kong)',
cls.KOREA: '韓国 (Korea)',
cls.TAIWAN: '台湾 (Taiwan)',
# The following are different depending on the version of the game,
# so we choose the new value.
cls.THAILAND: "タイ (Thailand)",
cls.INDONESIA: "インドネシア (Indonesia)",
cls.SINGAPORE: "シンガポール (Singapore)",
cls.PHILLIPINES: "フィリピン (Phillipines)",
cls.MACAO: "マカオ (Macao)",
cls.USA: "アメリカ (USA)",
cls.EUROPE: '欧州 (Europe)',
cls.NO_MAPPING: "海外 (Other)",
}
# This is just so I can use the defined constants inside a LUT
# without having the LUT itself outside the class.
RegionConstants = _RegionConstants()

View File

@ -3,7 +3,7 @@ import os
from sqlalchemy.engine import Engine # type: ignore from sqlalchemy.engine import Engine # type: ignore
from typing import Any, Dict, Optional, Set from typing import Any, Dict, Optional, Set
from bemani.common.constants import GameConstants from bemani.common import GameConstants, RegionConstants
from bemani.data.types import ArcadeID from bemani.data.types import ArcadeID
@ -79,6 +79,18 @@ class Server:
def pcbid_self_grant_limit(self) -> int: def pcbid_self_grant_limit(self) -> int:
return int(self.__config.get('server', {}).get('pcbid_self_grant_limit', 0)) return int(self.__config.get('server', {}).get('pcbid_self_grant_limit', 0))
@property
def region(self) -> int:
region = int(self.__config.get('server', {}).get('region', RegionConstants.USA))
if region in {RegionConstants.EUROPE, RegionConstants.NO_MAPPING}:
# Bogus values we support.
return region
if region < RegionConstants.MIN or region > RegionConstants.MAX:
# Pick the original default for the network (USA).
return RegionConstants.USA
# Region was fine.
return region
class Client: class Client:
def __init__(self, parent_config: "Config") -> None: def __init__(self, parent_config: "Config") -> None:

View File

@ -0,0 +1,35 @@
"""Add region column to arcade settings.
Revision ID: a1f4a09a1f90
Revises: 36dff3ac15a3
Create Date: 2021-09-06 22:01:29.905538
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import text
# revision identifiers, used by Alembic.
revision = 'a1f4a09a1f90'
down_revision = '36dff3ac15a3'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('arcade', sa.Column('pref', sa.Integer(), nullable=False))
# ### end Alembic commands ###
conn = op.get_bind()
# Set a sane default for all current arcades.
sql = "UPDATE arcade SET pref = 56"
conn.execute(text(sql), {})
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('arcade', 'pref')
# ### end Alembic commands ###

View File

@ -39,6 +39,7 @@ arcade = Table(
Column('name', String(255), nullable=False), Column('name', String(255), nullable=False),
Column('description', String(255), nullable=False), Column('description', String(255), nullable=False),
Column('pin', String(8), nullable=False), Column('pin', String(8), nullable=False),
Column('pref', Integer, nullable=False),
Column('data', JSON), Column('data', JSON),
mysql_charset='utf8mb4', mysql_charset='utf8mb4',
) )
@ -288,7 +289,7 @@ class MachineData(BaseData):
sql = "DELETE FROM `machine` WHERE pcbid = :pcbid LIMIT 1" sql = "DELETE FROM `machine` WHERE pcbid = :pcbid LIMIT 1"
self.execute(sql, {'pcbid': pcbid}) self.execute(sql, {'pcbid': pcbid})
def create_arcade(self, name: str, description: str, data: Dict[str, Any], owners: List[UserID]) -> Arcade: def create_arcade(self, name: str, description: str, region: int, data: Dict[str, Any], owners: List[UserID]) -> Arcade:
""" """
Given a set of values, create a new arcade and return the ID of that arcade. Given a set of values, create a new arcade and return the ID of that arcade.
@ -296,14 +297,15 @@ class MachineData(BaseData):
An Arcade object representing this arcade An Arcade object representing this arcade
""" """
sql = ( sql = (
"INSERT INTO arcade (name, description, data, pin) " + "INSERT INTO arcade (name, description, pref, data, pin) " +
"VALUES (:name, :desc, :data, '00000000')" "VALUES (:name, :desc, :pref, :data, '00000000')"
) )
cursor = self.execute( cursor = self.execute(
sql, sql,
{ {
'name': name, 'name': name,
'desc': description, 'desc': description,
'pref': region,
'data': self.serialize(data), 'data': self.serialize(data),
}, },
) )
@ -329,7 +331,7 @@ class MachineData(BaseData):
An Arcade object if this arcade was found, or None otherwise. An Arcade object if this arcade was found, or None otherwise.
""" """
sql = ( sql = (
"SELECT name, description, pin, data FROM arcade WHERE id = :id" "SELECT name, description, pin, pref, data FROM arcade WHERE id = :id"
) )
cursor = self.execute(sql, {'id': arcadeid}) cursor = self.execute(sql, {'id': arcadeid})
if cursor.rowcount != 1: if cursor.rowcount != 1:
@ -346,6 +348,7 @@ class MachineData(BaseData):
result['name'], result['name'],
result['description'], result['description'],
result['pin'], result['pin'],
result['pref'],
self.deserialize(result['data']), self.deserialize(result['data']),
[owner['userid'] for owner in cursor.fetchall()], [owner['userid'] for owner in cursor.fetchall()],
) )
@ -360,7 +363,7 @@ class MachineData(BaseData):
# Update machine name based on game # Update machine name based on game
sql = ( sql = (
"UPDATE `arcade` " + "UPDATE `arcade` " +
"SET name = :name, description = :desc, pin = :pin, data = :data " + "SET name = :name, description = :desc, pin = :pin, pref = :pref, data = :data " +
"WHERE id = :arcadeid" "WHERE id = :arcadeid"
) )
self.execute( self.execute(
@ -369,6 +372,7 @@ class MachineData(BaseData):
'name': arcade.name, 'name': arcade.name,
'desc': arcade.description, 'desc': arcade.description,
'pin': arcade.pin, 'pin': arcade.pin,
'pref': arcade.region,
'data': self.serialize(arcade.data), 'data': self.serialize(arcade.data),
'arcadeid': arcade.id, 'arcadeid': arcade.id,
}, },
@ -411,7 +415,7 @@ class MachineData(BaseData):
arcade_to_owners[arcade] = [] arcade_to_owners[arcade] = []
arcade_to_owners[arcade].append(owner) arcade_to_owners[arcade].append(owner)
sql = "SELECT id, name, description, pin, data FROM arcade" sql = "SELECT id, name, description, pin, pref, data FROM arcade"
cursor = self.execute(sql) cursor = self.execute(sql)
return [ return [
Arcade( Arcade(
@ -419,6 +423,7 @@ class MachineData(BaseData):
result['name'], result['name'],
result['description'], result['description'],
result['pin'], result['pin'],
result['pref'],
self.deserialize(result['data']), self.deserialize(result['data']),
arcade_to_owners.get(result['id'], []), arcade_to_owners.get(result['id'], []),
) for result in cursor.fetchall() ) for result in cursor.fetchall()

View File

@ -144,7 +144,7 @@ class Arcade:
crediting accounts. Machines belong to either no arcade or a single arcase. crediting accounts. Machines belong to either no arcade or a single arcase.
""" """
def __init__(self, arcadeid: ArcadeID, name: str, description: str, pin: str, data: Dict[str, Any], owners: List[UserID]) -> None: def __init__(self, arcadeid: ArcadeID, name: str, description: str, pin: str, region: int, data: Dict[str, Any], owners: List[UserID]) -> None:
""" """
Initialize the arcade instance. Initialize the arcade instance.
@ -153,6 +153,7 @@ class Arcade:
name - The name of the arcade. name - The name of the arcade.
description - The description of the arcade. description - The description of the arcade.
pin - An eight digit string representing the PIN used to pull up PASELI info. pin - An eight digit string representing the PIN used to pull up PASELI info.
region - An integer representing the region this arcade is in.
data - A dictionary of settings for this arcade. data - A dictionary of settings for this arcade.
owners - An list of integers specifying the user IDs of owners for this arcade. owners - An list of integers specifying the user IDs of owners for this arcade.
""" """
@ -160,11 +161,12 @@ class Arcade:
self.name = name self.name = name
self.description = description self.description = description
self.pin = pin self.pin = pin
self.region = region
self.data = ValidatedDict(data) self.data = ValidatedDict(data)
self.owners = owners self.owners = owners
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Arcade(arcadeid={self.id}, name={self.name}, description={self.description}, pin={self.pin}, data={self.data}, owners={self.owners})" return f"Arcade(arcadeid={self.id}, name={self.name}, description={self.description}, pin={self.pin}, region={self.region}, data={self.data}, owners={self.owners})"
class Song: class Song:

View File

@ -3,7 +3,7 @@ from typing import Dict, Tuple, Any, Optional
from flask import Blueprint, request, Response, render_template, url_for from flask import Blueprint, request, Response, render_template, url_for
from bemani.backend.base import Base from bemani.backend.base import Base
from bemani.common import CardCipher, CardCipherException, GameConstants from bemani.common import CardCipher, CardCipherException, GameConstants, RegionConstants
from bemani.data import Arcade, Machine, User, UserID, News, Event, Server, Client from bemani.data import Arcade, Machine, User, UserID, News, Event, Server, Client
from bemani.data.api.client import APIClient, NotAuthorizedAPIException, APIException 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.app import adminrequired, jsonify, valid_email, valid_username, valid_pin, render_react
@ -33,6 +33,7 @@ def format_arcade(arcade: Arcade) -> Dict[str, Any]:
'id': arcade.id, 'id': arcade.id,
'name': arcade.name, 'name': arcade.name,
'description': arcade.description, 'description': arcade.description,
'region': arcade.region,
'paseli_enabled': arcade.data.get_bool('paseli_enabled'), 'paseli_enabled': arcade.data.get_bool('paseli_enabled'),
'paseli_infinite': arcade.data.get_bool('paseli_infinite'), 'paseli_infinite': arcade.data.get_bool('paseli_infinite'),
'mask_services_url': arcade.data.get_bool('mask_services_url'), 'mask_services_url': arcade.data.get_bool('mask_services_url'),
@ -128,6 +129,7 @@ def viewsettings() -> Response:
**{ **{
'title': 'Network Settings', 'title': 'Network Settings',
'config': g.config, 'config': g.config,
'region': RegionConstants.LUT,
}, },
)) ))
@ -213,9 +215,11 @@ def viewarcades() -> Response:
'admin/arcades.react.js', 'admin/arcades.react.js',
{ {
'arcades': [format_arcade(arcade) for arcade in g.data.local.machine.get_all_arcades()], 'arcades': [format_arcade(arcade) for arcade in g.data.local.machine.get_all_arcades()],
'regions': RegionConstants.LUT,
'usernames': g.data.local.user.get_all_usernames(), 'usernames': g.data.local.user.get_all_usernames(),
'paseli_enabled': g.config.paseli.enabled, 'paseli_enabled': g.config.paseli.enabled,
'paseli_infinite': g.config.paseli.infinite, 'paseli_infinite': g.config.paseli.infinite,
'default_region': g.config.server.region,
'mask_services_url': False, 'mask_services_url': False,
}, },
{ {
@ -402,6 +406,7 @@ def updatearcade() -> Dict[str, Any]:
arcade.name = new_values['name'] arcade.name = new_values['name']
arcade.description = new_values['description'] arcade.description = new_values['description']
arcade.region = new_values['region']
arcade.data.replace_bool('paseli_enabled', new_values['paseli_enabled']) arcade.data.replace_bool('paseli_enabled', new_values['paseli_enabled'])
arcade.data.replace_bool('paseli_infinite', new_values['paseli_infinite']) arcade.data.replace_bool('paseli_infinite', new_values['paseli_infinite'])
arcade.data.replace_bool('mask_services_url', new_values['mask_services_url']) arcade.data.replace_bool('mask_services_url', new_values['mask_services_url'])
@ -441,6 +446,7 @@ def addarcade() -> Dict[str, Any]:
g.data.local.machine.create_arcade( g.data.local.machine.create_arcade(
new_values['name'], new_values['name'],
new_values['description'], new_values['description'],
new_values['region'],
{ {
'paseli_enabled': new_values['paseli_enabled'], 'paseli_enabled': new_values['paseli_enabled'],
'paseli_infinite': new_values['paseli_infinite'], 'paseli_infinite': new_values['paseli_infinite'],

View File

@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional
from flask import Blueprint, request, Response, abort, url_for from flask import Blueprint, request, Response, abort, url_for
from bemani.backend.base import Base from bemani.backend.base import Base
from bemani.common import CardCipher, CardCipherException, ValidatedDict, GameConstants from bemani.common import CardCipher, CardCipherException, ValidatedDict, GameConstants, RegionConstants
from bemani.data import Arcade, ArcadeID, Event, Machine from bemani.data import Arcade, ArcadeID, Event, Machine
from bemani.frontend.app import loginrequired, jsonify, render_react, valid_pin from bemani.frontend.app import loginrequired, jsonify, render_react, valid_pin
from bemani.frontend.templates import templates_location from bemani.frontend.templates import templates_location
@ -66,6 +66,7 @@ def format_arcade(arcade: Arcade) -> Dict[str, Any]:
'name': arcade.name, 'name': arcade.name,
'description': arcade.description, 'description': arcade.description,
'pin': arcade.pin, 'pin': arcade.pin,
'region': arcade.region,
'paseli_enabled': arcade.data.get_bool('paseli_enabled'), 'paseli_enabled': arcade.data.get_bool('paseli_enabled'),
'paseli_infinite': arcade.data.get_bool('paseli_infinite'), 'paseli_infinite': arcade.data.get_bool('paseli_infinite'),
'mask_services_url': arcade.data.get_bool('mask_services_url'), 'mask_services_url': arcade.data.get_bool('mask_services_url'),
@ -155,6 +156,7 @@ def viewarcade(arcadeid: int) -> Response:
'arcade/arcade.react.js', 'arcade/arcade.react.js',
{ {
'arcade': format_arcade(arcade), 'arcade': format_arcade(arcade),
'regions': RegionConstants.LUT,
'machines': machines, 'machines': machines,
'game_settings': get_game_settings(arcade), 'game_settings': get_game_settings(arcade),
'balances': {balance[0]: balance[1] for balance in g.data.local.machine.get_balances(arcadeid)}, 'balances': {balance[0]: balance[1] for balance in g.data.local.machine.get_balances(arcadeid)},
@ -172,6 +174,7 @@ def viewarcade(arcadeid: int) -> Response:
'add_balance': url_for('arcade_pages.addbalance', arcadeid=arcadeid), 'add_balance': url_for('arcade_pages.addbalance', arcadeid=arcadeid),
'update_balance': url_for('arcade_pages.updatebalance', arcadeid=arcadeid), 'update_balance': url_for('arcade_pages.updatebalance', arcadeid=arcadeid),
'update_pin': url_for('arcade_pages.updatepin', arcadeid=arcadeid), 'update_pin': url_for('arcade_pages.updatepin', arcadeid=arcadeid),
'update_region': url_for('arcade_pages.updateregion', arcadeid=arcadeid),
'generatepcbid': url_for('arcade_pages.generatepcbid', arcadeid=arcadeid), 'generatepcbid': url_for('arcade_pages.generatepcbid', arcadeid=arcadeid),
'updatepcbid': url_for('arcade_pages.updatepcbid', arcadeid=arcadeid), 'updatepcbid': url_for('arcade_pages.updatepcbid', arcadeid=arcadeid),
'removepcbid': url_for('arcade_pages.removepcbid', arcadeid=arcadeid), 'removepcbid': url_for('arcade_pages.removepcbid', arcadeid=arcadeid),
@ -306,6 +309,34 @@ def updatepin(arcadeid: int) -> Dict[str, Any]:
return {'pin': pin} return {'pin': pin}
@arcade_pages.route('/<int:arcadeid>/region/update', methods=['POST'])
@jsonify
@loginrequired
def updateregion(arcadeid: int) -> Dict[str, Any]:
# Cast the ID for type safety.
arcadeid = ArcadeID(arcadeid)
try:
region = int(request.get_json()['region'])
except Exception:
region = 0
# Make sure the arcade is valid
arcade = g.data.local.machine.get_arcade(arcadeid)
if arcade is None or g.userID not in arcade.owners:
raise Exception('You don\'t own this arcade, refusing to update!')
if region not in {RegionConstants.EUROPE, RegionConstants.NO_MAPPING} and (region < RegionConstants.MIN or region > RegionConstants.MAX):
raise Exception('Invalid region!')
# Update and save
arcade.region = region
g.data.local.machine.put_arcade(arcade)
# Return nothing
return {'region': region}
@arcade_pages.route('/<int:arcadeid>/pcbids/generate', methods=['POST']) @arcade_pages.route('/<int:arcadeid>/pcbids/generate', methods=['POST'])
@jsonify @jsonify
@loginrequired @loginrequired

View File

@ -6,7 +6,7 @@ from typing import Any, Dict, Iterator, List, Optional, Set, Tuple
from flask_caching import Cache # type: ignore from flask_caching import Cache # type: ignore
from bemani.common import GameConstants, Profile, ValidatedDict, ID from bemani.common import GameConstants, Profile, ValidatedDict, ID
from bemani.data import Data, Score, Attempt, Link, Song, UserID, RemoteUser from bemani.data import Data, Config, Score, Attempt, Link, Song, UserID, RemoteUser
class FrontendBase(ABC): class FrontendBase(ABC):
@ -34,7 +34,7 @@ class FrontendBase(ABC):
""" """
valid_rival_types: List[str] = [] valid_rival_types: List[str] = []
def __init__(self, data: Data, config: Dict[str, Any], cache: Cache) -> None: def __init__(self, data: Data, config: Config, cache: Cache) -> None:
self.data = data self.data = data
self.config = config self.config = config
self.cache = cache self.cache = cache

View File

@ -6,7 +6,7 @@ from flask_caching import Cache # type: ignore
from bemani.backend.bishi import BishiBashiFactory from bemani.backend.bishi import BishiBashiFactory
from bemani.common import Profile, ValidatedDict, ID, GameConstants from bemani.common import Profile, ValidatedDict, ID, GameConstants
from bemani.data import Data from bemani.data import Data, Config
from bemani.frontend.base import FrontendBase from bemani.frontend.base import FrontendBase
@ -14,7 +14,7 @@ class BishiBashiFrontend(FrontendBase):
game = GameConstants.BISHI_BASHI game = GameConstants.BISHI_BASHI
def __init__(self, data: Data, config: Dict[str, Any], cache: Cache) -> None: def __init__(self, data: Data, config: Config, cache: Cache) -> None:
super().__init__(data, config, cache) super().__init__(data, config, cache)
self.machines: Dict[int, str] = {} self.machines: Dict[int, str] = {}

View File

@ -3,7 +3,7 @@ import re
from typing import Any, Dict from typing import Any, Dict
from flask import Blueprint, request, Response, url_for, abort from flask import Blueprint, request, Response, url_for, abort
from bemani.common import ID, GameConstants from bemani.common import ID, GameConstants, RegionConstants
from bemani.data import UserID from bemani.data import UserID
from bemani.frontend.app import loginrequired, jsonify, render_react from bemani.frontend.app import loginrequired, jsonify, render_react
from bemani.frontend.iidx.iidx import IIDXFrontend from bemani.frontend.iidx.iidx import IIDXFrontend
@ -326,6 +326,7 @@ def viewsettings() -> Response:
'iidx/settings.react.js', 'iidx/settings.react.js',
{ {
'player': djinfo, 'player': djinfo,
'regions': RegionConstants.LUT,
'versions': {version: name for (game, version, name) in frontend.all_games()}, 'versions': {version: name for (game, version, name) in frontend.all_games()},
'qpros': frontend.get_all_items(versions), 'qpros': frontend.get_all_items(versions),
}, },
@ -515,7 +516,7 @@ def updateprefecture() -> Dict[str, Any]:
profile = g.data.local.user.get_profile(GameConstants.IIDX, version, user.id) profile = g.data.local.user.get_profile(GameConstants.IIDX, version, user.id)
if profile is None: if profile is None:
raise Exception('Unable to find profile to update!') raise Exception('Unable to find profile to update!')
profile.replace_int('pid', prefecture) profile.replace_int('pid', RegionConstants.db_to_game_region(version >= 25, prefecture))
g.data.local.user.put_profile(GameConstants.IIDX, version, user.id, profile) g.data.local.user.put_profile(GameConstants.IIDX, version, user.id, profile)
# Return that we updated # Return that we updated

View File

@ -4,8 +4,8 @@ from typing import Any, Dict, Iterator, Optional, Tuple, List
from flask_caching import Cache # type: ignore from flask_caching import Cache # type: ignore
from bemani.backend.iidx import IIDXFactory, IIDXBase from bemani.backend.iidx import IIDXFactory, IIDXBase
from bemani.common import Profile, ValidatedDict, GameConstants from bemani.common import Profile, ValidatedDict, GameConstants, RegionConstants
from bemani.data import Attempt, Data, Score, Song, UserID from bemani.data import Attempt, Data, Config, Score, Song, UserID
from bemani.frontend.base import FrontendBase from bemani.frontend.base import FrontendBase
@ -27,7 +27,7 @@ class IIDXFrontend(FrontendBase):
'dp_rival', 'dp_rival',
] ]
def __init__(self, data: Data, config: Dict[str, Any], cache: Cache) -> None: def __init__(self, data: Data, config: Config, cache: Cache) -> None:
super().__init__(data, config, cache) super().__init__(data, config, cache)
self.machines: Dict[int, str] = {} self.machines: Dict[int, str] = {}
@ -189,7 +189,7 @@ class IIDXFrontend(FrontendBase):
formatted_profile = super().format_profile(profile, playstats) formatted_profile = super().format_profile(profile, playstats)
formatted_profile.update({ formatted_profile.update({
'arcade': "", 'arcade': "",
'prefecture': profile.get_int('pid', 51), 'prefecture': RegionConstants.game_to_db_region(profile.version >= 25, profile.get_int('pid', self.config.server.region)),
'settings': self.format_settings(profile.get_dict('settings')), 'settings': self.format_settings(profile.get_dict('settings')),
'flags': self.format_flags(profile.get_dict('settings')), 'flags': self.format_flags(profile.get_dict('settings')),
'sdjp': playstats.get_int('single_dj_points'), 'sdjp': playstats.get_int('single_dj_points'),

View File

@ -6,7 +6,7 @@ from flask_caching import Cache # type: ignore
from bemani.backend.mga import MetalGearArcadeFactory from bemani.backend.mga import MetalGearArcadeFactory
from bemani.common import Profile, ValidatedDict, ID, GameConstants from bemani.common import Profile, ValidatedDict, ID, GameConstants
from bemani.data import Data from bemani.data import Data, Config
from bemani.frontend.base import FrontendBase from bemani.frontend.base import FrontendBase
@ -14,7 +14,7 @@ class MetalGearArcadeFrontend(FrontendBase):
game = GameConstants.MGA game = GameConstants.MGA
def __init__(self, data: Data, config: Dict[str, Any], cache: Cache) -> None: def __init__(self, data: Data, config: Config, cache: Cache) -> None:
super().__init__(data, config, cache) super().__init__(data, config, cache)
self.machines: Dict[int, str] = {} self.machines: Dict[int, str] = {}

View File

@ -5,7 +5,7 @@ from flask_caching import Cache # type: ignore
from bemani.backend.museca import MusecaFactory, MusecaBase from bemani.backend.museca import MusecaFactory, MusecaBase
from bemani.common import GameConstants, VersionConstants, DBConstants, Profile, ValidatedDict from bemani.common import GameConstants, VersionConstants, DBConstants, Profile, ValidatedDict
from bemani.data import Attempt, Data, Score, Song, UserID from bemani.data import Attempt, Data, Config, Score, Song, UserID
from bemani.frontend.base import FrontendBase from bemani.frontend.base import FrontendBase
@ -19,7 +19,7 @@ class MusecaFrontend(FrontendBase):
MusecaBase.CHART_TYPE_RED, MusecaBase.CHART_TYPE_RED,
] ]
def __init__(self, data: Data, config: Dict[str, Any], cache: Cache) -> None: def __init__(self, data: Data, config: Config, cache: Cache) -> None:
super().__init__(data, config, cache) super().__init__(data, config, cache)
def all_games(self) -> Iterator[Tuple[GameConstants, int, str]]: def all_games(self) -> Iterator[Tuple[GameConstants, int, str]]:

View File

@ -5,7 +5,7 @@ from flask_caching import Cache # type: ignore
from bemani.backend.reflec import ReflecBeatFactory, ReflecBeatBase from bemani.backend.reflec import ReflecBeatFactory, ReflecBeatBase
from bemani.common import GameConstants, Profile, ValidatedDict from bemani.common import GameConstants, Profile, ValidatedDict
from bemani.data import Attempt, Data, Score, Song, UserID from bemani.data import Attempt, Data, Config, Score, Song, UserID
from bemani.frontend.base import FrontendBase from bemani.frontend.base import FrontendBase
@ -26,7 +26,7 @@ class ReflecBeatFrontend(FrontendBase):
'rival', 'rival',
] ]
def __init__(self, data: Data, config: Dict[str, Any], cache: Cache) -> None: def __init__(self, data: Data, config: Config, cache: Cache) -> None:
super().__init__(data, config, cache) super().__init__(data, config, cache)
def all_games(self) -> Iterator[Tuple[GameConstants, int, str]]: def all_games(self) -> Iterator[Tuple[GameConstants, int, str]]:

View File

@ -5,7 +5,7 @@ from flask_caching import Cache # type: ignore
from bemani.backend.sdvx import SoundVoltexFactory, SoundVoltexBase from bemani.backend.sdvx import SoundVoltexFactory, SoundVoltexBase
from bemani.common import GameConstants, Profile, ValidatedDict from bemani.common import GameConstants, Profile, ValidatedDict
from bemani.data import Attempt, Data, Score, Song, UserID from bemani.data import Attempt, Data, Config, Score, Song, UserID
from bemani.frontend.base import FrontendBase from bemani.frontend.base import FrontendBase
@ -25,7 +25,7 @@ class SoundVoltexFrontend(FrontendBase):
'rival', 'rival',
] ]
def __init__(self, data: Data, config: Dict[str, Any], cache: Cache) -> None: def __init__(self, data: Data, config: Config, cache: Cache) -> None:
super().__init__(data, config, cache) super().__init__(data, config, cache)
def all_games(self) -> Iterator[Tuple[GameConstants, int, str]]: def all_games(self) -> Iterator[Tuple[GameConstants, int, str]]:

View File

@ -6,6 +6,7 @@ var card_management = React.createClass({
new_arcade: { new_arcade: {
name: '', name: '',
description: '', description: '',
region: window.default_region,
paseli_enabled: window.paseli_enabled, paseli_enabled: window.paseli_enabled,
paseli_infinite: window.paseli_infinite, paseli_infinite: window.paseli_infinite,
mask_services_url: window.mask_services_url, mask_services_url: window.mask_services_url,
@ -34,6 +35,7 @@ var card_management = React.createClass({
new_arcade: { new_arcade: {
name: '', name: '',
description: '', description: '',
region: window.default_region,
paseli_enabled: window.paseli_enabled, paseli_enabled: window.paseli_enabled,
paseli_infinite: window.paseli_infinite, paseli_infinite: window.paseli_infinite,
mask_services_url: window.mask_services_url, mask_services_url: window.mask_services_url,
@ -182,6 +184,23 @@ var card_management = React.createClass({
} }
}, },
renderRegion: function(arcade) {
if (this.state.editing_arcade && arcade.id == this.state.editing_arcade.id) {
return <SelectInt
name="region"
value={ this.state.editing_arcade.region }
choices={ window.regions }
onChange={function(choice) {
var arcade = this.state.editing_arcade;
arcade.region = event.target.value;
this.setState({editing_arcade: arcade});
}.bind(this)}
/>;
} else {
return <span>{ window.regions[arcade.region] }</span>;
}
},
sortDescription: function(a, b) { sortDescription: function(a, b) {
return a.description.localeCompare(b.description); return a.description.localeCompare(b.description);
}, },
@ -309,6 +328,10 @@ var card_management = React.createClass({
render: this.renderDescription, render: this.renderDescription,
sort: this.sortDescription, sort: this.sortDescription,
}, },
{
name: "Region",
render: this.renderRegion,
},
{ {
name: 'Owners', name: 'Owners',
render: this.renderOwners, render: this.renderOwners,
@ -344,6 +367,7 @@ var card_management = React.createClass({
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Description</th> <th>Description</th>
<th>Region</th>
<th>Owners</th> <th>Owners</th>
<th>PASELI Enabled</th> <th>PASELI Enabled</th>
<th>PASELI Infinite</th> <th>PASELI Infinite</th>
@ -377,6 +401,18 @@ var card_management = React.createClass({
}.bind(this)} }.bind(this)}
/> />
</td> </td>
<td>
<SelectInt
name="region"
value={ this.state.new_arcade.region }
choices={ window.regions }
onChange={function(choice) {
var arcade = this.state.new_arcade;
arcade.region = event.target.value;
this.setState({new_arcade: arcade});
}.bind(this)}
/>
</td>
<td>{ <td>{
this.state.new_arcade.owners.map(function(owner, index) { this.state.new_arcade.owners.map(function(owner, index) {
return ( return (

View File

@ -32,6 +32,9 @@ var arcade_management = React.createClass({
pin: window.arcade.pin, pin: window.arcade.pin,
editing_pin: false, editing_pin: false,
new_pin: '', new_pin: '',
region: window.arcade.region,
editing_region: false,
new_region: '',
paseli_enabled_saving: false, paseli_enabled_saving: false,
paseli_infinite_saving: false, paseli_infinite_saving: false,
mask_services_url_saving: false, mask_services_url_saving: false,
@ -103,6 +106,21 @@ var arcade_management = React.createClass({
event.preventDefault(); event.preventDefault();
}, },
saveRegion: function(event) {
AJAX.post(
Link.get('update_region'),
{region: this.state.new_region},
function(response) {
this.setState({
region: response.region,
new_region: '',
editing_region: false,
});
}.bind(this)
);
event.preventDefault();
},
getSettingIndex: function(setting_name) { getSettingIndex: function(setting_name) {
var real_index = -1; var real_index = -1;
this.state.settings.map(function(game_settings, index) { this.state.settings.map(function(game_settings, index) {
@ -296,6 +314,46 @@ var arcade_management = React.createClass({
); );
}, },
renderRegion: function() {
return (
<LabelledSection vertical={true} label="Region">{
!this.state.editing_region ?
<span>
<span>{ window.regions[this.state.region] }</span>
<Edit
onClick={function(event) {
this.setState({editing_region: true, new_region: this.state.region});
}.bind(this)}
/>
</span> :
<form className="inline" onSubmit={this.saveRegion}>
<SelectInt
name="region"
value={ this.state.new_region }
choices={ window.regions }
onChange={function(choice) {
this.setState({new_region: event.target.value});
}.bind(this)}
/>
<input
type="submit"
value="save"
/>
<input
type="button"
value="cancel"
onClick={function(event) {
this.setState({
new_region: '',
editing_region: false,
});
}.bind(this)}
/>
</form>
}</LabelledSection>
);
},
generateNewMachine: function(event) { generateNewMachine: function(event) {
AJAX.post( AJAX.post(
Link.get('generatepcbid'), Link.get('generatepcbid'),
@ -478,6 +536,7 @@ var arcade_management = React.createClass({
<span className="placeholder">no description</span> <span className="placeholder">no description</span>
}</LabelledSection> }</LabelledSection>
{this.renderPIN()} {this.renderPIN()}
{this.renderRegion()}
<LabelledSection vertical={true} label="PASELI Enabled"> <LabelledSection vertical={true} label="PASELI Enabled">
<span>{ this.state.paseli_enabled ? 'yes' : 'no' }</span> <span>{ this.state.paseli_enabled ? 'yes' : 'no' }</span>
<Toggle onClick={this.togglePaseliEnabled.bind(this)} /> <Toggle onClick={this.togglePaseliEnabled.bind(this)} />

View File

@ -314,12 +314,11 @@ var settings_view = React.createClass({
}, },
renderPrefecture: function(player) { renderPrefecture: function(player) {
regions = this.state.version >= 25 ? Regions2 : Regions;
return ( return (
<LabelledSection vertical={true} label="Prefecture">{ <LabelledSection vertical={true} label="Prefecture">{
!this.state.editing_prefecture ? !this.state.editing_prefecture ?
<span> <span>
<span>{regions[player.prefecture]}</span> <span>{window.regions[player.prefecture]}</span>
<Edit <Edit
onClick={function(event) { onClick={function(event) {
this.setState({editing_prefecture: true}); this.setState({editing_prefecture: true});
@ -330,7 +329,7 @@ var settings_view = React.createClass({
<SelectInt <SelectInt
name="prefecture" name="prefecture"
value={this.state.new_prefecture} value={this.state.new_prefecture}
choices={regions} choices={window.regions}
onChange={function(choice) { onChange={function(choice) {
this.setState({new_prefecture: choice}); this.setState({new_prefecture: choice});
}.bind(this)} }.bind(this)}

View File

@ -1,117 +0,0 @@
var Regions = [
null,
"北海道",
"青森県",
"岩手県",
"宮城県",
"秋田県",
"山形県",
"福島県",
"茨城県",
"栃木県",
"群馬県",
"埼玉県",
"千葉県",
"東京都",
"神奈川県",
"新潟県",
"富山県",
"石川県",
"福井県",
"山梨県",
"長野県",
"岐阜県",
"静岡県",
"愛知県",
"三重県",
"滋賀県",
"京都府",
"大阪府",
"兵庫県",
"奈良県",
"和歌山県",
"鳥取県",
"島根県",
"岡山県",
"広島県",
"山口県",
"徳島県",
"香川県",
"愛媛県",
"高知県",
"福岡県",
"佐賀県",
"長崎県",
"熊本県",
"大分県",
"宮崎県",
"鹿児島県",
"沖縄県",
"香港",
"韓国",
"台湾",
"米国 (USA)",
"欧州 (Europe)",
"海外 (Other)",
];
Regions2 = [
null,
"北海道",
"青森県",
"岩手県",
"宮城県",
"秋田県",
"山形県",
"福島県",
"茨城県",
"栃木県",
"群馬県",
"埼玉県",
"千葉県",
"東京都",
"神奈川県",
"新潟県",
"富山県",
"石川県",
"福井県",
"山梨県",
"長野県",
"岐阜県",
"静岡県",
"愛知県",
"三重県",
"滋賀県",
"京都府",
"大阪府",
"兵庫県",
"奈良県",
"和歌山県",
"鳥取県",
"島根県",
"岡山県",
"広島県",
"山口県",
"徳島県",
"香川県",
"愛媛県",
"高知県",
"福岡県",
"佐賀県",
"長崎県",
"熊本県",
"大分県",
"宮崎県",
"鹿児島県",
"沖縄県",
"香港",
"韓国",
"台湾",
"タイ",
"インドネシア",
"シンガポール",
"フィリピン",
"マカオ",
"アメリカ",
"海外 (Other)",
];

View File

@ -65,6 +65,8 @@
<dd>{{ 'yes' if config.paseli.enabled else 'no' }} (can be overridden by arcade settings)</dd> <dd>{{ 'yes' if config.paseli.enabled else 'no' }} (can be overridden by arcade settings)</dd>
<dt>Infinite PASELI Enabled</dt> <dt>Infinite PASELI Enabled</dt>
<dd>{{ 'yes' if config.paseli.infinite else 'no' }} (can be overridden by arcade settings)</dd> <dd>{{ 'yes' if config.paseli.infinite else 'no' }} (can be overridden by arcade settings)</dd>
<dt>Default Region</dt>
<dd>{{ region[config.server.region] }} (can be overridden by arcade settings)</dd>
<dt>Event Log Preservation Duration</dt> <dt>Event Log Preservation Duration</dt>
<dd>{{ (config.event_log_duration|string + ' seconds') if config.event_log_duration else 'infinite' }}</dd> <dd>{{ (config.event_log_duration|string + ' seconds') if config.event_log_duration else 'infinite' }}</dd>
</dl> </dl>

View File

@ -33,6 +33,11 @@ server:
# page. Note that this setting is irrelevant if PCBID enforcing is off. # page. Note that this setting is irrelevant if PCBID enforcing is off.
# Set to 0 or delete this setting to disable self-granting PCBIDs. # Set to 0 or delete this setting to disable self-granting PCBIDs.
pcbid_self_grant_limit: 0 pcbid_self_grant_limit: 0
# Default region for this network (set to USA by default). See RegionConstants
# for details on acceptible values. The range of accepted values is 1-56 matching
# the 56 normal regions found in RegionConstants, and 1000 for "Europe" and
# 2000 for "Other".
region: 56
# Webhook URLs. These allow for game scores from games with scorecard support to be broadcasted to outside services. # Webhook URLs. These allow for game scores from games with scorecard support to be broadcasted to outside services.
# Delete this to disable this support. # Delete this to disable this support.