diff --git a/bemani/backend/core/core.py b/bemani/backend/core/core.py index fe0a7fa..669f6e5 100644 --- a/bemani/backend/core/core.py +++ b/bemani/backend/core/core.py @@ -129,9 +129,11 @@ class CoreHandler(Base): machine = self.get_machine() if machine.arcade is None: region = self.config.server.region + area = self.config.server.area else: arcade = self.data.local.machine.get_arcade(machine.arcade) region = arcade.region + area = arcade.area if region == RegionConstants.HONG_KONG: country = "HK" @@ -182,6 +184,9 @@ class CoreHandler(Base): location.add_child(Node.string("region", regionstr)) location.add_child(Node.string("name", machine.name)) location.add_child(Node.u8("type", 0)) + if area is not None: + location.add_child(Node.string("regionname", area)) + location.add_child(Node.string("regionjname", area)) line = Node.void("line") line.add_child(Node.string("id", ".")) diff --git a/bemani/common/id.py b/bemani/common/id.py index 526e7e3..454094f 100644 --- a/bemani/common/id.py +++ b/bemani/common/id.py @@ -45,7 +45,19 @@ class ID: """ Take a machine ID as an integer, format it as a string. """ - if region not in {"JP", "KR", "TW", "HK", "US", "GB", "IT", "ES", "FR", "PT"}: + if region not in { + "JP", + "KR", + "TW", + "HK", + "US", + "GB", + "IT", + "ES", + "FR", + "PT", + "XX", + }: raise Exception(f"Invalid region {region}!") return f"{region}-{machine_id}" @@ -57,7 +69,7 @@ class ID: try: if ( machine_id[:2] - in {"JP", "KR", "TW", "HK", "US", "GB", "IT", "ES", "FR", "PT"} + in {"JP", "KR", "TW", "HK", "US", "GB", "IT", "ES", "FR", "PT", "XX"} and machine_id[2] == "-" ): return int(machine_id[3:]) diff --git a/bemani/data/config.py b/bemani/data/config.py index dc8f3e8..7505bf4 100644 --- a/bemani/data/config.py +++ b/bemani/data/config.py @@ -95,6 +95,11 @@ class Server: # Region was fine. return region + @property + def area(self) -> Optional[str]: + area = self.__config.get("server", {}).get("area") + return str(area) if area else None + class Client: def __init__(self, parent_config: "Config") -> None: diff --git a/bemani/data/migrations/versions/f64d138962e0_add_custom_area_string_to_arcade_.py b/bemani/data/migrations/versions/f64d138962e0_add_custom_area_string_to_arcade_.py new file mode 100644 index 0000000..7c24c39 --- /dev/null +++ b/bemani/data/migrations/versions/f64d138962e0_add_custom_area_string_to_arcade_.py @@ -0,0 +1,28 @@ +"""Add custom area string to arcade configuration. + +Revision ID: f64d138962e0 +Revises: a1f4a09a1f90 +Create Date: 2022-10-15 21:23:02.461661 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f64d138962e0' +down_revision = 'a1f4a09a1f90' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('arcade', sa.Column('area', sa.String(length=63), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('arcade', 'area') + # ### end Alembic commands ### diff --git a/bemani/data/mysql/machine.py b/bemani/data/mysql/machine.py index 319ae53..229d9d7 100644 --- a/bemani/data/mysql/machine.py +++ b/bemani/data/mysql/machine.py @@ -41,6 +41,7 @@ arcade = Table( Column("description", String(255), nullable=False), Column("pin", String(8), nullable=False), Column("pref", Integer, nullable=False), + Column("area", String(63)), Column("data", JSON), mysql_charset="utf8mb4", ) @@ -317,6 +318,7 @@ class MachineData(BaseData): name: str, description: str, region: int, + area: Optional[str], data: Dict[str, Any], owners: List[UserID], ) -> Arcade: @@ -327,8 +329,8 @@ class MachineData(BaseData): An Arcade object representing this arcade """ sql = ( - "INSERT INTO arcade (name, description, pref, data, pin) " - + "VALUES (:name, :desc, :pref, :data, '00000000')" + "INSERT INTO arcade (name, description, pref, area, data, pin) " + + "VALUES (:name, :desc, :pref, :area, :data, '00000000')" ) cursor = self.execute( sql, @@ -336,6 +338,7 @@ class MachineData(BaseData): "name": name, "desc": description, "pref": region, + "area": area, "data": self.serialize(data), }, ) @@ -362,7 +365,9 @@ class MachineData(BaseData): Returns: An Arcade object if this arcade was found, or None otherwise. """ - sql = "SELECT name, description, pin, pref, data FROM arcade WHERE id = :id" + sql = ( + "SELECT name, description, pin, pref, area, data FROM arcade WHERE id = :id" + ) cursor = self.execute(sql, {"id": arcadeid}) if cursor.rowcount != 1: # Arcade doesn't exist @@ -379,6 +384,7 @@ class MachineData(BaseData): result["description"], result["pin"], result["pref"], + result["area"] or None, self.deserialize(result["data"]), [owner["userid"] for owner in cursor.fetchall()], ) @@ -393,7 +399,7 @@ class MachineData(BaseData): # Update machine name based on game sql = ( "UPDATE `arcade` " - + "SET name = :name, description = :desc, pin = :pin, pref = :pref, data = :data " + + "SET name = :name, description = :desc, pin = :pin, pref = :pref, area = :area, data = :data " + "WHERE id = :arcadeid" ) self.execute( @@ -403,6 +409,7 @@ class MachineData(BaseData): "desc": arcade.description, "pin": arcade.pin, "pref": arcade.region, + "area": arcade.area, "data": self.serialize(arcade.data), "arcadeid": arcade.id, }, @@ -447,7 +454,7 @@ class MachineData(BaseData): arcade_to_owners[arcade] = [] arcade_to_owners[arcade].append(owner) - sql = "SELECT id, name, description, pin, pref, data FROM arcade" + sql = "SELECT id, name, description, pin, pref, area, data FROM arcade" cursor = self.execute(sql) return [ Arcade( @@ -456,6 +463,7 @@ class MachineData(BaseData): result["description"], result["pin"], result["pref"], + result["area"] or None, self.deserialize(result["data"]), arcade_to_owners.get(result["id"], []), ) diff --git a/bemani/data/types.py b/bemani/data/types.py index 7d4d2e5..e942c5f 100644 --- a/bemani/data/types.py +++ b/bemani/data/types.py @@ -161,6 +161,7 @@ class Arcade: description: str, pin: str, region: int, + area: Optional[str], data: Dict[str, Any], owners: List[UserID], ) -> None: @@ -173,6 +174,7 @@ class Arcade: description - The description of the arcade. pin - An eight digit string representing the PIN used to pull up PASELI info. region - An integer representing the region this arcade is in. + area - A string representing the custom area this arcade is in, or None if default. data - A dictionary of settings for this arcade. owners - An list of integers specifying the user IDs of owners for this arcade. """ @@ -181,11 +183,12 @@ class Arcade: self.description = description self.pin = pin self.region = region + self.area = area self.data = ValidatedDict(data) self.owners = owners def __repr__(self) -> str: - return f"Arcade(arcadeid={self.id}, name={self.name}, description={self.description}, pin={self.pin}, region={self.region}, data={self.data}, owners={self.owners})" + return f"Arcade(arcadeid={self.id}, name={self.name}, description={self.description}, pin={self.pin}, region={self.region}, area={self.area}, data={self.data}, owners={self.owners})" class Song: diff --git a/bemani/frontend/admin/admin.py b/bemani/frontend/admin/admin.py index b51b68c..58fafd9 100644 --- a/bemani/frontend/admin/admin.py +++ b/bemani/frontend/admin/admin.py @@ -48,6 +48,7 @@ def format_arcade(arcade: Arcade) -> Dict[str, Any]: "name": arcade.name, "description": arcade.description, "region": arcade.region, + "area": arcade.area or "", "paseli_enabled": arcade.data.get_bool("paseli_enabled"), "paseli_infinite": arcade.data.get_bool("paseli_infinite"), "mask_services_url": arcade.data.get_bool("mask_services_url"), @@ -269,6 +270,7 @@ def viewarcades() -> Response: "paseli_enabled": g.config.paseli.enabled, "paseli_infinite": g.config.paseli.infinite, "default_region": g.config.server.region, + "default_area": g.config.server.area, "mask_services_url": False, }, { @@ -500,6 +502,7 @@ def updatearcade() -> Dict[str, Any]: arcade.name = new_values["name"] arcade.description = new_values["description"] arcade.region = new_values["region"] + arcade.area = new_values["area"] or None arcade.data.replace_bool("paseli_enabled", new_values["paseli_enabled"]) arcade.data.replace_bool("paseli_infinite", new_values["paseli_infinite"]) arcade.data.replace_bool("mask_services_url", new_values["mask_services_url"]) @@ -542,6 +545,7 @@ def addarcade() -> Dict[str, Any]: new_values["name"], new_values["description"], new_values["region"], + new_values["area"] or None, { "paseli_enabled": new_values["paseli_enabled"], "paseli_infinite": new_values["paseli_infinite"], diff --git a/bemani/frontend/arcade/arcade.py b/bemani/frontend/arcade/arcade.py index 8af1939..8ccf66b 100644 --- a/bemani/frontend/arcade/arcade.py +++ b/bemani/frontend/arcade/arcade.py @@ -76,6 +76,7 @@ def format_arcade(arcade: Arcade) -> Dict[str, Any]: "description": arcade.description, "pin": arcade.pin, "region": arcade.region, + "area": arcade.area, "paseli_enabled": arcade.data.get_bool("paseli_enabled"), "paseli_infinite": arcade.data.get_bool("paseli_infinite"), "mask_services_url": arcade.data.get_bool("mask_services_url"), @@ -155,6 +156,7 @@ def viewarcade(arcadeid: int) -> Response: "update_balance": url_for("arcade_pages.updatebalance", arcadeid=arcadeid), "update_pin": url_for("arcade_pages.updatepin", arcadeid=arcadeid), "update_region": url_for("arcade_pages.updateregion", arcadeid=arcadeid), + "update_area": url_for("arcade_pages.updatearea", arcadeid=arcadeid), "generatepcbid": url_for("arcade_pages.generatepcbid", arcadeid=arcadeid), "updatepcbid": url_for("arcade_pages.updatepcbid", arcadeid=arcadeid), "removepcbid": url_for("arcade_pages.removepcbid", arcadeid=arcadeid), @@ -344,6 +346,31 @@ def updateregion(arcadeid: int) -> Dict[str, Any]: return {"region": region} +@arcade_pages.route("//area/update", methods=["POST"]) +@jsonify +@loginrequired +def updatearea(arcadeid: int) -> Dict[str, Any]: + # Cast the ID for type safety. + arcadeid = ArcadeID(arcadeid) + + try: + area = request.get_json()["area"] or None + except Exception: + area = None + + # 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!") + + # Update and save + arcade.area = area + g.data.local.machine.put_arcade(arcade) + + # Return nothing + return {"area": area} + + @arcade_pages.route("//pcbids/generate", methods=["POST"]) @jsonify @loginrequired diff --git a/bemani/frontend/static/controllers/admin/arcades.react.js b/bemani/frontend/static/controllers/admin/arcades.react.js index ce43b1f..856373d 100644 --- a/bemani/frontend/static/controllers/admin/arcades.react.js +++ b/bemani/frontend/static/controllers/admin/arcades.react.js @@ -7,6 +7,7 @@ var card_management = createReactClass({ name: '', description: '', region: window.default_region, + area: window.default_area, paseli_enabled: window.paseli_enabled, paseli_infinite: window.paseli_infinite, mask_services_url: window.mask_services_url, @@ -36,6 +37,7 @@ var card_management = createReactClass({ name: '', description: '', region: window.default_region, + area: window.default_area, paseli_enabled: window.paseli_enabled, paseli_infinite: window.paseli_infinite, mask_services_url: window.mask_services_url, @@ -201,10 +203,37 @@ var card_management = createReactClass({ } }, + renderArea: function(arcade) { + if (this.state.editing_arcade && arcade.id == this.state.editing_arcade.id) { + return ; + } else { + return { arcade.area }; + } + }, + sortDescription: function(a, b) { return a.description.localeCompare(b.description); }, + sortArea: function(a, b) { + return a.area.localeCompare(b.area); + }, + + sortRegion: function(a, b) { + return window.regions[a.region].localeCompare(window.regions[b.region]); + }, + renderOwners: function(arcade) { if (this.state.editing_arcade && arcade.id == this.state.editing_arcade.id) { return this.state.editing_arcade.owners.map(function(owner, index) { @@ -331,6 +360,12 @@ var card_management = createReactClass({ { name: "Region", render: this.renderRegion, + sort: this.sortRegion, + }, + { + name: "Custom Area", + render: this.renderArea, + sort: this.sortArea, }, { name: 'Owners', @@ -368,6 +403,7 @@ var card_management = createReactClass({ Name Description Region + Custom Area Owners PASELI Enabled PASELI Infinite @@ -413,6 +449,18 @@ var card_management = createReactClass({ }.bind(this)} /> + + + { this.state.new_arcade.owners.map(function(owner, index) { return ( diff --git a/bemani/frontend/static/controllers/arcade/arcade.react.js b/bemani/frontend/static/controllers/arcade/arcade.react.js index d3d2b66..26a37ae 100644 --- a/bemani/frontend/static/controllers/arcade/arcade.react.js +++ b/bemani/frontend/static/controllers/arcade/arcade.react.js @@ -27,6 +27,9 @@ var arcade_management = createReactClass({ region: window.arcade.region, editing_region: false, new_region: '', + area: window.arcade.area, + editing_area: false, + new_area: '', paseli_enabled_saving: false, paseli_infinite_saving: false, mask_services_url_saving: false, @@ -105,6 +108,21 @@ var arcade_management = createReactClass({ event.preventDefault(); }, + saveArea: function(event) { + AJAX.post( + Link.get('update_area'), + {area: this.state.new_area}, + function(response) { + this.setState({ + area: response.area, + new_area: '', + editing_area: false, + }); + }.bind(this) + ); + event.preventDefault(); + }, + togglePaseliEnabled: function() { this.setState({paseli_enabled_saving: true}) AJAX.post( @@ -291,6 +309,51 @@ var arcade_management = createReactClass({ ); }, + renderArea: function() { + return ( + { + !this.state.editing_area ? + <> + { this.state.area ? this.state.area : unset } + + : +
+ (this.focus_element = c)} + value={this.state.new_area} + onChange={function(event) { + if (event.target.value.length <= 63) { + this.setState({new_area: event.target.value}); + } + }.bind(this)} + name="area" + /> + + +
+ }
+ ); + }, + generateNewMachine: function(event) { AJAX.post( Link.get('generatepcbid'), @@ -474,6 +537,7 @@ var arcade_management = createReactClass({ } {this.renderPIN()} {this.renderRegion()} + {this.renderArea()} PASELI Enabled diff --git a/bemani/frontend/templates/admin/settings.html b/bemani/frontend/templates/admin/settings.html index 62b4d00..9a66b0f 100644 --- a/bemani/frontend/templates/admin/settings.html +++ b/bemani/frontend/templates/admin/settings.html @@ -67,6 +67,12 @@
{{ 'yes' if config.paseli.infinite else 'no' }} (can be overridden by arcade settings)
Default Region
{{ region[config.server.region] }} (can be overridden by arcade settings)
+
Default Custom Area
+ {% if config.server.area %} +
{{ config.server.area or "unset" }} (can be overridden by arcade settings)
+ {% else %} +
unset (can be overridden by arcade settings)
+ {% endif %}
Event Log Preservation Duration
{{ (config.event_log_duration|string + ' seconds') if config.event_log_duration else 'infinite' }}
diff --git a/config/server.yaml b/config/server.yaml index 9f38b68..64f9c63 100644 --- a/config/server.yaml +++ b/config/server.yaml @@ -1,3 +1,4 @@ +# Core database settings, required so that the system can persist scores and accounts. database: # IP or DNS entry for MySQL instance. address: "localhost" @@ -12,6 +13,8 @@ database: # Set this to False or delete this to run in production mode. read_only: False +# Core server settings, required so that the backend knows what to tell games for core +# routing and server URLs. server: # Advertised server IP or DNS entry games will connect to. address: "192.168.0.1" @@ -38,6 +41,9 @@ server: # the 56 normal regions found in RegionConstants, and 1000 for "Europe" and # 2000 for "Other". region: 56 + # The custom area code displayed in the system settings menu on various games. + # Delete this setting to force games to display "Unobtained" instead. + area: "USA" # Webhook URLs. These allow for game scores from games with scorecard support to be broadcasted to outside services. # Delete this to disable this support. @@ -47,12 +53,16 @@ webhooks: - "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi" pnm: - "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi" + +# Global PASESLI settings, which can be overridden on a per-arcade basis. These form the default settings. paseli: # Whether PASELI is enabled on the network. enabled: True # Whether infinite PASELI balance is enabled on the network. infinite: True +# Game series to provide support for. Disabling something here hides it from the frontend and makes the backend +# ignore games coming from that series. support: # Bishi Bashi frontend/backend enabled. bishi: True