1
0
mirror of synced 2024-11-24 06:20:12 +01:00

Add ability to specify a custom area that gets picked up by some games.

This commit is contained in:
Jennifer Taylor 2022-10-15 22:05:08 +00:00
parent f11fa7de1d
commit 85a3f08c78
12 changed files with 228 additions and 8 deletions

View File

@ -129,9 +129,11 @@ class CoreHandler(Base):
machine = self.get_machine() machine = self.get_machine()
if machine.arcade is None: if machine.arcade is None:
region = self.config.server.region region = self.config.server.region
area = self.config.server.area
else: else:
arcade = self.data.local.machine.get_arcade(machine.arcade) arcade = self.data.local.machine.get_arcade(machine.arcade)
region = arcade.region region = arcade.region
area = arcade.area
if region == RegionConstants.HONG_KONG: if region == RegionConstants.HONG_KONG:
country = "HK" country = "HK"
@ -182,6 +184,9 @@ class CoreHandler(Base):
location.add_child(Node.string("region", regionstr)) location.add_child(Node.string("region", regionstr))
location.add_child(Node.string("name", machine.name)) location.add_child(Node.string("name", machine.name))
location.add_child(Node.u8("type", 0)) 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 = Node.void("line")
line.add_child(Node.string("id", ".")) line.add_child(Node.string("id", "."))

View File

@ -45,7 +45,19 @@ class ID:
""" """
Take a machine ID as an integer, format it as a string. 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}!") raise Exception(f"Invalid region {region}!")
return f"{region}-{machine_id}" return f"{region}-{machine_id}"
@ -57,7 +69,7 @@ class ID:
try: try:
if ( if (
machine_id[:2] 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] == "-" and machine_id[2] == "-"
): ):
return int(machine_id[3:]) return int(machine_id[3:])

View File

@ -95,6 +95,11 @@ class Server:
# Region was fine. # Region was fine.
return region return region
@property
def area(self) -> Optional[str]:
area = self.__config.get("server", {}).get("area")
return str(area) if area else None
class Client: class Client:
def __init__(self, parent_config: "Config") -> None: def __init__(self, parent_config: "Config") -> None:

View File

@ -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 ###

View File

@ -41,6 +41,7 @@ arcade = Table(
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("pref", Integer, nullable=False),
Column("area", String(63)),
Column("data", JSON), Column("data", JSON),
mysql_charset="utf8mb4", mysql_charset="utf8mb4",
) )
@ -317,6 +318,7 @@ class MachineData(BaseData):
name: str, name: str,
description: str, description: str,
region: int, region: int,
area: Optional[str],
data: Dict[str, Any], data: Dict[str, Any],
owners: List[UserID], owners: List[UserID],
) -> Arcade: ) -> Arcade:
@ -327,8 +329,8 @@ class MachineData(BaseData):
An Arcade object representing this arcade An Arcade object representing this arcade
""" """
sql = ( sql = (
"INSERT INTO arcade (name, description, pref, data, pin) " "INSERT INTO arcade (name, description, pref, area, data, pin) "
+ "VALUES (:name, :desc, :pref, :data, '00000000')" + "VALUES (:name, :desc, :pref, :area, :data, '00000000')"
) )
cursor = self.execute( cursor = self.execute(
sql, sql,
@ -336,6 +338,7 @@ class MachineData(BaseData):
"name": name, "name": name,
"desc": description, "desc": description,
"pref": region, "pref": region,
"area": area,
"data": self.serialize(data), "data": self.serialize(data),
}, },
) )
@ -362,7 +365,9 @@ class MachineData(BaseData):
Returns: Returns:
An Arcade object if this arcade was found, or None otherwise. 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}) cursor = self.execute(sql, {"id": arcadeid})
if cursor.rowcount != 1: if cursor.rowcount != 1:
# Arcade doesn't exist # Arcade doesn't exist
@ -379,6 +384,7 @@ class MachineData(BaseData):
result["description"], result["description"],
result["pin"], result["pin"],
result["pref"], result["pref"],
result["area"] or None,
self.deserialize(result["data"]), self.deserialize(result["data"]),
[owner["userid"] for owner in cursor.fetchall()], [owner["userid"] for owner in cursor.fetchall()],
) )
@ -393,7 +399,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, pref = :pref, data = :data " + "SET name = :name, description = :desc, pin = :pin, pref = :pref, area = :area, data = :data "
+ "WHERE id = :arcadeid" + "WHERE id = :arcadeid"
) )
self.execute( self.execute(
@ -403,6 +409,7 @@ class MachineData(BaseData):
"desc": arcade.description, "desc": arcade.description,
"pin": arcade.pin, "pin": arcade.pin,
"pref": arcade.region, "pref": arcade.region,
"area": arcade.area,
"data": self.serialize(arcade.data), "data": self.serialize(arcade.data),
"arcadeid": arcade.id, "arcadeid": arcade.id,
}, },
@ -447,7 +454,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, pref, data FROM arcade" sql = "SELECT id, name, description, pin, pref, area, data FROM arcade"
cursor = self.execute(sql) cursor = self.execute(sql)
return [ return [
Arcade( Arcade(
@ -456,6 +463,7 @@ class MachineData(BaseData):
result["description"], result["description"],
result["pin"], result["pin"],
result["pref"], result["pref"],
result["area"] or None,
self.deserialize(result["data"]), self.deserialize(result["data"]),
arcade_to_owners.get(result["id"], []), arcade_to_owners.get(result["id"], []),
) )

View File

@ -161,6 +161,7 @@ class Arcade:
description: str, description: str,
pin: str, pin: str,
region: int, region: int,
area: Optional[str],
data: Dict[str, Any], data: Dict[str, Any],
owners: List[UserID], owners: List[UserID],
) -> None: ) -> None:
@ -173,6 +174,7 @@ class 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. 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. 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.
""" """
@ -181,11 +183,12 @@ class Arcade:
self.description = description self.description = description
self.pin = pin self.pin = pin
self.region = region self.region = region
self.area = area
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}, 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: class Song:

View File

@ -48,6 +48,7 @@ def format_arcade(arcade: Arcade) -> Dict[str, Any]:
"name": arcade.name, "name": arcade.name,
"description": arcade.description, "description": arcade.description,
"region": arcade.region, "region": arcade.region,
"area": arcade.area or "",
"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"),
@ -269,6 +270,7 @@ def viewarcades() -> Response:
"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, "default_region": g.config.server.region,
"default_area": g.config.server.area,
"mask_services_url": False, "mask_services_url": False,
}, },
{ {
@ -500,6 +502,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.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_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"])
@ -542,6 +545,7 @@ def addarcade() -> Dict[str, Any]:
new_values["name"], new_values["name"],
new_values["description"], new_values["description"],
new_values["region"], new_values["region"],
new_values["area"] or None,
{ {
"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

@ -76,6 +76,7 @@ def format_arcade(arcade: Arcade) -> Dict[str, Any]:
"description": arcade.description, "description": arcade.description,
"pin": arcade.pin, "pin": arcade.pin,
"region": arcade.region, "region": arcade.region,
"area": arcade.area,
"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:
"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), "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), "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),
@ -344,6 +346,31 @@ def updateregion(arcadeid: int) -> Dict[str, Any]:
return {"region": region} return {"region": region}
@arcade_pages.route("/<int:arcadeid>/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("/<int:arcadeid>/pcbids/generate", methods=["POST"]) @arcade_pages.route("/<int:arcadeid>/pcbids/generate", methods=["POST"])
@jsonify @jsonify
@loginrequired @loginrequired

View File

@ -7,6 +7,7 @@ var card_management = createReactClass({
name: '', name: '',
description: '', description: '',
region: window.default_region, region: window.default_region,
area: window.default_area,
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,
@ -36,6 +37,7 @@ var card_management = createReactClass({
name: '', name: '',
description: '', description: '',
region: window.default_region, region: window.default_region,
area: window.default_area,
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,
@ -201,10 +203,37 @@ var card_management = createReactClass({
} }
}, },
renderArea: function(arcade) {
if (this.state.editing_arcade && arcade.id == this.state.editing_arcade.id) {
return <input
name="area"
type="text"
value={ this.state.editing_arcade.area }
onChange={function(event) {
var arcade = this.state.editing_arcade;
arcade.area = event.target.value;
this.setState({
editing_arcade: arcade,
});
}.bind(this)}
/>;
} else {
return <span>{ arcade.area }</span>;
}
},
sortDescription: function(a, b) { sortDescription: function(a, b) {
return a.description.localeCompare(b.description); 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) { renderOwners: function(arcade) {
if (this.state.editing_arcade && arcade.id == this.state.editing_arcade.id) { if (this.state.editing_arcade && arcade.id == this.state.editing_arcade.id) {
return this.state.editing_arcade.owners.map(function(owner, index) { return this.state.editing_arcade.owners.map(function(owner, index) {
@ -331,6 +360,12 @@ var card_management = createReactClass({
{ {
name: "Region", name: "Region",
render: this.renderRegion, render: this.renderRegion,
sort: this.sortRegion,
},
{
name: "Custom Area",
render: this.renderArea,
sort: this.sortArea,
}, },
{ {
name: 'Owners', name: 'Owners',
@ -368,6 +403,7 @@ var card_management = createReactClass({
<th>Name</th> <th>Name</th>
<th>Description</th> <th>Description</th>
<th>Region</th> <th>Region</th>
<th>Custom Area</th>
<th>Owners</th> <th>Owners</th>
<th>PASELI Enabled</th> <th>PASELI Enabled</th>
<th>PASELI Infinite</th> <th>PASELI Infinite</th>
@ -413,6 +449,18 @@ var card_management = createReactClass({
}.bind(this)} }.bind(this)}
/> />
</td> </td>
<td>
<input
name="area"
type="text"
value={ this.state.new_arcade.area }
onChange={function(event) {
var arcade = this.state.new_arcade;
arcade.area = 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

@ -27,6 +27,9 @@ var arcade_management = createReactClass({
region: window.arcade.region, region: window.arcade.region,
editing_region: false, editing_region: false,
new_region: '', new_region: '',
area: window.arcade.area,
editing_area: false,
new_area: '',
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,
@ -105,6 +108,21 @@ var arcade_management = createReactClass({
event.preventDefault(); 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() { togglePaseliEnabled: function() {
this.setState({paseli_enabled_saving: true}) this.setState({paseli_enabled_saving: true})
AJAX.post( AJAX.post(
@ -291,6 +309,51 @@ var arcade_management = createReactClass({
); );
}, },
renderArea: function() {
return (
<LabelledSection vertical={true} label="Custom Area">{
!this.state.editing_area ?
<>
<span>{ this.state.area ? this.state.area : <i>unset</i> }</span>
<Edit
onClick={function(event) {
this.setState({editing_area: true, new_area: this.state.area});
}.bind(this)}
/>
</> :
<form className="inline" onSubmit={this.saveArea}>
<input
type="text"
className="inline"
autofocus="true"
ref={c => (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"
/>
<input
type="submit"
value="save"
/>
<input
type="button"
value="cancel"
onClick={function(event) {
this.setState({
new_area: '',
editing_area: false,
});
}.bind(this)}
/>
</form>
}</LabelledSection>
);
},
generateNewMachine: function(event) { generateNewMachine: function(event) {
AJAX.post( AJAX.post(
Link.get('generatepcbid'), Link.get('generatepcbid'),
@ -474,6 +537,7 @@ var arcade_management = createReactClass({
}</LabelledSection> }</LabelledSection>
{this.renderPIN()} {this.renderPIN()}
{this.renderRegion()} {this.renderRegion()}
{this.renderArea()}
<LabelledSection vertical={true} label={ <LabelledSection vertical={true} label={
<> <>
PASELI Enabled PASELI Enabled

View File

@ -67,6 +67,12 @@
<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> <dt>Default Region</dt>
<dd>{{ region[config.server.region] }} (can be overridden by arcade settings)</dd> <dd>{{ region[config.server.region] }} (can be overridden by arcade settings)</dd>
<dt>Default Custom Area</dt>
{% if config.server.area %}
<dd>{{ config.server.area or "<i>unset</i>" }} (can be overridden by arcade settings)</dd>
{% else %}
<dd><i>unset</i> (can be overridden by arcade settings)</dd>
{% endif %}
<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

@ -1,3 +1,4 @@
# Core database settings, required so that the system can persist scores and accounts.
database: database:
# IP or DNS entry for MySQL instance. # IP or DNS entry for MySQL instance.
address: "localhost" address: "localhost"
@ -12,6 +13,8 @@ database:
# Set this to False or delete this to run in production mode. # Set this to False or delete this to run in production mode.
read_only: False read_only: False
# Core server settings, required so that the backend knows what to tell games for core
# routing and server URLs.
server: server:
# Advertised server IP or DNS entry games will connect to. # Advertised server IP or DNS entry games will connect to.
address: "192.168.0.1" address: "192.168.0.1"
@ -38,6 +41,9 @@ server:
# the 56 normal regions found in RegionConstants, and 1000 for "Europe" and # the 56 normal regions found in RegionConstants, and 1000 for "Europe" and
# 2000 for "Other". # 2000 for "Other".
region: 56 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. # 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.
@ -47,12 +53,16 @@ webhooks:
- "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi" - "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi"
pnm: pnm:
- "https://discord.com/api/webhooks/1232122131321321321/eauihfafaewfhjaveuijaewuivhjawueihoi" - "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: paseli:
# Whether PASELI is enabled on the network. # Whether PASELI is enabled on the network.
enabled: True enabled: True
# Whether infinite PASELI balance is enabled on the network. # Whether infinite PASELI balance is enabled on the network.
infinite: True 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: support:
# Bishi Bashi frontend/backend enabled. # Bishi Bashi frontend/backend enabled.
bishi: True bishi: True